Modules/businessdev.ALbuild.Core/Public/New-ALbuildCacheMutex.ps1

function New-ALbuildCacheMutex {
    <#
    .SYNOPSIS
        Opens the cross-process lock of an ALbuild cache entry, or returns $null when Windows refuses it.
 
    .DESCRIPTION
        Every cache that builds on one host share (artifacts, first-party symbols, call graph sources,
        images) is locked with a named mutex whose name comes from Get-ALbuildCacheLockName. Creating
        that mutex can fail for reasons that have nothing to do with the build: on 2026-11-03 the MCP
        server on SRV05 got "The system cannot open the device or file specified" (Win32 110) for about
        40 minutes, and every build that needed a new artifact failed, although nothing was wrong with
        the cache.
 
        The lock only prevents two builds from doing the same work at once; the caches themselves are
        published atomically (extract to a staging folder, then rename). So a failed lock must not fail
        the build. This function retries a few times, and when the mutex still cannot be opened it logs
        everything needed to diagnose it - folder, mutex name, error, Win32 code, account, session,
        process - and returns $null. The caller decides what $null means: work without the lock
        (extraction) or treat the entry as busy (removal).
 
    .PARAMETER Path
        The cache folder (or lock key) the mutex protects, as passed to Get-ALbuildCacheLockName.
 
    .PARAMETER RetryCount
        Further attempts after the first failure. Default 3.
 
    .PARAMETER RetryDelaySeconds
        Base delay between attempts; attempt n waits n times this. Default 2 (2 + 4 + 6 s).
 
    .EXAMPLE
        $mutex = New-ALbuildCacheMutex -Path $targetFolder
        if (-not $mutex) { Write-ALbuildLog -Level Warning 'Continuing without the cache lock.' }
 
    .OUTPUTS
        System.Threading.Mutex, or $null when it could not be opened.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Opens an in-memory lock handle; nothing on disk changes.')]
    [CmdletBinding()]
    [OutputType([System.Threading.Mutex])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Path,
        [ValidateRange(0, 10)] [int] $RetryCount = 3,
        [ValidateRange(0, 60)] [int] $RetryDelaySeconds = 2
    )

    $name = Get-ALbuildCacheLockName -Path $Path
    $attempt = 0
    while ($true) {
        $attempt++
        try { return (New-ALbuildNamedMutex -Name $name) }
        catch {
            $e = $_.Exception
            while ($e -is [System.Management.Automation.MethodInvocationException] -and $e.InnerException) { $e = $e.InnerException }
            $win32 = $e.HResult -band 0xFFFF
            if ($attempt -le $RetryCount) {
                Write-ALbuildLog -Level Verbose "Cache lock '$name' could not be opened (attempt $attempt, Win32 $win32): $($e.Message) Retrying."
                Start-Sleep -Seconds ($RetryDelaySeconds * $attempt)
                continue
            }
            # Environment, not WindowsIdentity: the module's CI also runs on Linux.
            $identity = if ([Environment]::UserDomainName) { "$([Environment]::UserDomainName)\$([Environment]::UserName)" } else { [Environment]::UserName }
            $process = [System.Diagnostics.Process]::GetCurrentProcess()
            Write-ALbuildLog -Level Warning ("Cache lock not available after $attempt attempt(s): $($e.Message) " +
                "[folder '$Path', mutex '$name', $($e.GetType().FullName), HResult 0x$('{0:X8}' -f $e.HResult), Win32 $win32, " +
                "account '$identity', session $($process.SessionId), process $($process.Id)]")
            return $null
        }
    }
}