Modules/businessdev.ALbuild.Core/Public/Lock-ALbuildCacheEntry.ps1

function Lock-ALbuildCacheEntry {
    <#
    .SYNOPSIS
        Acquires the cache locks that cover a prunable cache entry.
 
    .DESCRIPTION
        A cache entry may not be removed while another build is extracting into it. Get-BcArtifact locks
        the LEAF it extracts into (<version>\<country>, <version>\platform), not the version folder, so
        covering an entry means locking the entry itself plus every immediate child.
 
        All-or-nothing: if any lock cannot be taken within the timeout, the ones already held are
        released before returning, so a caller can never hold half a lock set. The lock name comes from
        Get-ALbuildCacheLockName, which is the single definition both sides must agree on - a second,
        copied definition would drift and then silently delete a folder out from under a reader.
 
    .PARAMETER Path
        The cache entry to cover (typically a <type>\<version> folder).
 
    .PARAMETER TimeoutSeconds
        How long to wait per lock. Default 5.
 
    .EXAMPLE
        $locks = Lock-ALbuildCacheEntry -Path $versionFolder
        if ($locks.Acquired) { try { Remove-Item ... } finally { Unlock-ALbuildCacheEntry -Locks $locks } }
 
    .OUTPUTS
        PSCustomObject with Acquired, BusyPath and Mutexes.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path,
        [int] $TimeoutSeconds = 5
    )

    $paths = @($Path) + @(
        Get-ChildItem -LiteralPath $Path -Directory -Force -ErrorAction SilentlyContinue |
            ForEach-Object { $_.FullName }
    )
    $held = [System.Collections.Generic.List[object]]::new()
    foreach ($p in $paths) {
        $mutex = New-Object System.Threading.Mutex($false, (Get-ALbuildCacheLockName -Path $p))
        $got = $false
        try { $got = $mutex.WaitOne([TimeSpan]::FromSeconds($TimeoutSeconds)) }
        catch [System.Threading.AbandonedMutexException] { $got = $true }  # prior holder crashed; we own it
        if (-not $got) {
            $mutex.Dispose()
            foreach ($h in $held) { $h.ReleaseMutex(); $h.Dispose() }
            return [PSCustomObject]@{ Acquired = $false; BusyPath = $p; Mutexes = @() }
        }
        $held.Add($mutex)
    }
    return [PSCustomObject]@{ Acquired = $true; BusyPath = $null; Mutexes = $held.ToArray() }
}