lib/Lock.ps1
|
function Open-SingleInstanceLock { <# .SYNOPSIS Acquires an exclusive file lock so only one instance of the script can run at a time. Returns a lock object on success, or $null when another instance already holds the lock. .DESCRIPTION The lock file is opened with FileShare.None, so the OS rejects any second process trying to open it with the same exclusive intent. The lock is automatically released when the FileStream is disposed or the process exits (including crash / Ctrl+C / hard kill). The PID and start timestamp are written into the file so you can inspect it (e.g. `Get-Content`) to see who's holding the lock. #> [CmdletBinding()] param() $base = [Environment]::GetFolderPath('LocalApplicationData') $lockDir = Join-Path $base 'Initialize-DeveloperMachine' $lockFile = Join-Path $lockDir 'run.lock' # Use direct .NET API rather than New-Item so the directory is created # even under -WhatIf - the lock dir is internal infrastructure, not # user state subject to dry-run semantics. [void][System.IO.Directory]::CreateDirectory($lockDir) $stream = $null try { $stream = [System.IO.File]::Open( $lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None ) } catch [System.IO.IOException] { # An IOException here typically means another instance has the file # locked with FileShare.None. Anything else (DirectoryNotFound, # UnauthorizedAccess, ...) propagates so the user sees the real cause. if ($_.Exception -is [System.IO.FileNotFoundException] -or $_.Exception -is [System.IO.DirectoryNotFoundException] -or $_.Exception -is [System.UnauthorizedAccessException]) { throw } return $null } # Write a marker so the lock file is informative for debugging. $stream.SetLength(0) $bytes = [System.Text.Encoding]::UTF8.GetBytes( "PID $PID started $(Get-Date -Format 'o')`r`n" ) $stream.Write($bytes, 0, $bytes.Length) $stream.Flush() [PSCustomObject]@{ Stream = $stream Path = $lockFile } } function Close-SingleInstanceLock { <# .SYNOPSIS Releases the file lock acquired by Open-SingleInstanceLock and removes the lock file. Safe to call with $null (no-op). #> [CmdletBinding()] param( [Parameter()] $Lock ) if ($null -eq $Lock) { return } if ($Lock.Stream) { $Lock.Stream.Dispose() } # Use direct .NET API rather than Remove-Item so cleanup happens even # under -WhatIf - the lock file is internal infrastructure, not user # state subject to dry-run semantics. if ($Lock.Path) { try { [System.IO.File]::Delete($Lock.Path) } catch { } } } |