Functions/GenXdev.FileSystem/WriteJsonAtomic.ps1
function WriteJsonAtomic { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$FilePath, [Parameter(Mandatory = $true)] [hashtable]$Data, [Parameter(Mandatory = $false)] [int]$MaxRetries = 10, [Parameter(Mandatory = $false)] [int]$RetryDelayMs = 200 ) # ensure directory exists $directory = [System.IO.Path]::GetDirectoryName($FilePath) if (-not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $directory)) { $null = GenXdev.FileSystem\Expand-Path $directory -CreateDirectory } # construct file paths for atomic operation $lockFile = "${FilePath}.lock" $tmpFile = "${FilePath}.tmp" $tmp2File = "${FilePath}.tmp2" # attempt atomic write with retries for ($attempt = 0; $attempt -lt $MaxRetries; $attempt++) { try { # clean up stale lock files older than 30 seconds if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $lockFile) { $lockInfo = [System.IO.FileInfo]::new($lockFile) $ageMilliSeconds = ([DateTime]::Now - $lockInfo.LastWriteTime).TotalMilliseconds if ($ageMilliSeconds -gt ($MaxRetries * 10)) { Microsoft.PowerShell.Utility\Write-Verbose ` "Removing stale lock file: $lockFile (age: ${ageMilliSeconds}ms)" Microsoft.PowerShell.Management\Remove-Item ` -LiteralPath $lockFile ` -Force ` -ErrorAction SilentlyContinue } else { # lock is recent, wait and retry throw "File is locked by another process" } } # create lock file $null = Microsoft.PowerShell.Management\New-Item ` -Path $lockFile ` -ItemType File ` -Force ` -ErrorAction Stop try { # write json content to temporary file $jsonContent = $Data | Microsoft.PowerShell.Utility\ConvertTo-Json ` -Depth 10 ` -Compress:$false [System.IO.File]::WriteAllText($tmpFile, $jsonContent, ` [System.Text.Encoding]::UTF8) # perform atomic rename operation if (Microsoft.PowerShell.Management\Test-Path ` -LiteralPath $FilePath) { # rename existing file to .tmp2 Microsoft.PowerShell.Management\Move-Item ` -LiteralPath $FilePath ` -Destination $tmp2File ` -Force ` -ErrorAction Stop } # rename .tmp to actual filename Microsoft.PowerShell.Management\Move-Item ` -LiteralPath $tmpFile ` -Destination $FilePath ` -Force ` -ErrorAction Stop # delete backup file if it exists if (Microsoft.PowerShell.Management\Test-Path ` -LiteralPath $tmp2File) { Microsoft.PowerShell.Management\Remove-Item ` -LiteralPath $tmp2File ` -Force ` -ErrorAction SilentlyContinue } # operation successful, break retry loop break } finally { # always remove lock file if (Microsoft.PowerShell.Management\Test-Path ` -LiteralPath $lockFile) { Microsoft.PowerShell.Management\Remove-Item ` -LiteralPath $lockFile ` -Force ` -ErrorAction SilentlyContinue } } } catch { # log retry attempt Microsoft.PowerShell.Utility\Write-Verbose ` "Write attempt $($attempt + 1) failed: $($_.Exception.Message)" # clean up temporary files on error foreach ($tempFile in @($tmpFile, $tmp2File, $lockFile)) { if (Microsoft.PowerShell.Management\Test-Path ` -LiteralPath $tempFile) { Microsoft.PowerShell.Management\Remove-Item ` -LiteralPath $tempFile ` -Force ` -ErrorAction SilentlyContinue } } # wait before retry unless this is the last attempt if ($attempt -lt ($MaxRetries - 1)) { Microsoft.PowerShell.Utility\Start-Sleep ` -Milliseconds $RetryDelayMs } else { # final attempt failed, throw error throw "Failed to write JSON file after ${MaxRetries} attempts: $_" } } } } |