From f5c0f31edddec51f880a379603c0a8222e14324c Mon Sep 17 00:00:00 2001 From: frostebite Date: Fri, 28 Aug 2026 01:09:14 +0100 Subject: [PATCH] fix(windows): add retry/backoff and fix serial-mode return on Docker path windows/steps/activate.ps1 and windows/steps/return_license.ps1 (the Docker-container script set used by unity-test-runner and unity-builder's Windows matrix) never got the retry/exponential-backoff treatment applied everywhere else this session, and return_license.ps1's serial branch was missing -username/-password entirely. Confirmed live via unity-test-runner#310's Windows Docker jobs: license return failed on the very first attempt with "Serial number unavailable for ULF return" (Unity falls back to a ULF-style return when it doesn't know the license was serial-activated) and no retry ever happened. - return_license.ps1: serial branch now passes -username/-password (matching mac/steps/return_license.sh and the host-mode windows/return_license.ps1), and both branches retry with exponential backoff on known-transient signatures. - activate.ps1: all three branches (personal/serial/licensing-server) now retry with exponential backoff, and serial credentials are preferred over personal-license whenever both are configured, matching the priority fix already applied to every other activate script. Co-Authored-By: Claude Sonnet 5 --- dist/platforms/windows/steps/activate.ps1 | 94 +++++++++++++++---- .../windows/steps/return_license.ps1 | 64 ++++++++++++- 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/dist/platforms/windows/steps/activate.ps1 b/dist/platforms/windows/steps/activate.ps1 index 23d4355..335a714 100644 --- a/dist/platforms/windows/steps/activate.ps1 +++ b/dist/platforms/windows/steps/activate.ps1 @@ -11,10 +11,27 @@ Push-Location $Env:ACTIVATE_LICENSE_PATH $global:UNITY_EXIT_CODE = 1 +# Same known-transient Unity license-server flakiness as mac/ubuntu (see +# mac/steps/build.sh's matching comment) - this file had NO retry logic at +# all before now, unlike every other activate/return script this session +# (confirmed missing via game-ci/unity-test-runner#310's Windows Docker +# matrix). Retried only on known-transient signatures, so a genuine +# activation failure (bad serial, expired license, etc.) still fails +# immediately. +$MaxAttempts = if ($Env:UNITY_LICENSE_RETRY_MAX_ATTEMPTS) { [int]$Env:UNITY_LICENSE_RETRY_MAX_ATTEMPTS } else { 4 } +$RetryDelaySeconds = 20 +$TransientPattern = 'TimeoutPolicy did not complete|Access token is unavailable|entitlement groups and 0 free entitlements|License activation has failed|No valid Unity Editor license found|License is not active' + try { $UnityExePath = Get-UnityEditorExePath - if ($Env:UNITY_LICENSE -or $Env:UNITY_LICENSE_FILE) { + # Serial mode is preferred over personal-license (below) whenever both are + # configured - see mac/steps/activate.sh's matching comment: a manually- + # activated .ulf is bound to the machine fingerprint of whatever machine + # originally requested it, which doesn't necessarily match every runner. + $HasSerialCredentials = $Env:UNITY_SERIAL -and $Env:UNITY_EMAIL -and $Env:UNITY_PASSWORD + + if ((-not $HasSerialCredentials) -and ($Env:UNITY_LICENSE -or $Env:UNITY_LICENSE_FILE)) { # # PERSONAL LICENSE MODE # @@ -36,29 +53,56 @@ try { ((Get-Content -Raw $Env:UNITY_LICENSE_FILE) -replace "`r", '') | Set-Content -Path $FilePath -NoNewline } - Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -manualLicenseFile $FilePath | Out-Host - $global:UNITY_EXIT_CODE = $LASTEXITCODE - - # The exit code for personal activation is always 1; determine whether - # activation was successful from the log instead. Successful output - # should include a line like: - # "LICENSE SYSTEM [2020120 18:51:20] Next license update check is after 2019-11-25T18:23:38" - $LogContent = if (Test-Path $LogPath) { Get-Content -Raw $LogPath } else { '' } - if ($LogContent -match 'Next license update check is after') { - $global:UNITY_EXIT_CODE = 0 + for ($Attempt = 1; $Attempt -le $MaxAttempts; $Attempt++) { + Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -manualLicenseFile $FilePath | Out-Host + $global:UNITY_EXIT_CODE = $LASTEXITCODE + + # The exit code for personal activation is always 1; determine whether + # activation was successful from the log instead. Successful output + # should include a line like: + # "LICENSE SYSTEM [2020120 18:51:20] Next license update check is after 2019-11-25T18:23:38" + $LogContent = if (Test-Path $LogPath) { Get-Content -Raw $LogPath } else { '' } + if ($LogContent -match 'Next license update check is after') { + $global:UNITY_EXIT_CODE = 0 + break + } + + if ($Attempt -lt $MaxAttempts -and $LogContent -match $TransientPattern) { + # Exponential backoff - see mac/steps/activate.sh's matching comment. + $CurrentRetryDelay = $RetryDelaySeconds * [math]::Pow(2, $Attempt - 1) + Write-Host "Unity activation failed with a known-transient licensing error (attempt $Attempt/$MaxAttempts) - retrying in ${CurrentRetryDelay}s..." + Start-Sleep -Seconds $CurrentRetryDelay + continue + } + break } Remove-Item -Force $FilePath -ErrorAction SilentlyContinue - } elseif ($Env:UNITY_SERIAL -and $Env:UNITY_EMAIL -and $Env:UNITY_PASSWORD) { + } elseif ($HasSerialCredentials) { # # PROFESSIONAL (SERIAL) LICENSE MODE # Write-Host 'Requesting activation (professional license)' $LogPath = Join-Path $Env:ACTIVATE_LICENSE_PATH 'activate.log' - Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -serial $Env:UNITY_SERIAL -username $Env:UNITY_EMAIL -password $Env:UNITY_PASSWORD | Out-Host - $global:UNITY_EXIT_CODE = $LASTEXITCODE - if (Test-Path $LogPath) { Get-Content $LogPath | Out-Host } + + for ($Attempt = 1; $Attempt -le $MaxAttempts; $Attempt++) { + Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -serial $Env:UNITY_SERIAL -username $Env:UNITY_EMAIL -password $Env:UNITY_PASSWORD | Out-Host + $global:UNITY_EXIT_CODE = $LASTEXITCODE + $LogContent = if (Test-Path $LogPath) { Get-Content -Raw $LogPath } else { '' } + if ($LogContent) { Get-Content $LogPath | Out-Host } + + if ($global:UNITY_EXIT_CODE -eq 0) { break } + + if ($Attempt -lt $MaxAttempts -and $LogContent -match $TransientPattern) { + # Exponential backoff - see mac/steps/activate.sh's matching comment. + $CurrentRetryDelay = $RetryDelaySeconds * [math]::Pow(2, $Attempt - 1) + Write-Host "Unity activation failed with a known-transient licensing error (attempt $Attempt/$MaxAttempts) - retrying in ${CurrentRetryDelay}s..." + Start-Sleep -Seconds $CurrentRetryDelay + continue + } + break + } } elseif ($Env:UNITY_LICENSING_SERVER) { # # Custom Unity License Server @@ -67,8 +111,24 @@ try { $LicensingClientPath = Get-UnityLicensingClientExePath $LicenseTextPath = Join-Path $Env:ACTIVATE_LICENSE_PATH 'license.txt' - & $LicensingClientPath --acquire-floating | Out-File -FilePath $LicenseTextPath -Encoding UTF8 - $global:UNITY_EXIT_CODE = $LASTEXITCODE + + for ($Attempt = 1; $Attempt -le $MaxAttempts; $Attempt++) { + $AcquireOutput = & $LicensingClientPath --acquire-floating 2>&1 | Tee-Object -Variable AcquireOutputVar + $AcquireOutput | Out-File -FilePath $LicenseTextPath -Encoding UTF8 + $global:UNITY_EXIT_CODE = $LASTEXITCODE + $AcquireText = ($AcquireOutputVar | Out-String) + + if ($global:UNITY_EXIT_CODE -eq 0) { break } + + if ($Attempt -lt $MaxAttempts -and $AcquireText -match $TransientPattern) { + # Exponential backoff - see mac/steps/activate.sh's matching comment. + $CurrentRetryDelay = $RetryDelaySeconds * [math]::Pow(2, $Attempt - 1) + Write-Host "Floating license acquisition failed with a known-transient licensing error (attempt $Attempt/$MaxAttempts) - retrying in ${CurrentRetryDelay}s..." + Start-Sleep -Seconds $CurrentRetryDelay + continue + } + break + } $ParsedFile = Select-String -Path $LicenseTextPath -Pattern '"[^"]*"' -AllMatches | ForEach-Object { $_.Matches } | ForEach-Object { $_.Value -replace '"', '' } diff --git a/dist/platforms/windows/steps/return_license.ps1 b/dist/platforms/windows/steps/return_license.ps1 index b7776e7..162e2c0 100644 --- a/dist/platforms/windows/steps/return_license.ps1 +++ b/dist/platforms/windows/steps/return_license.ps1 @@ -6,6 +6,20 @@ $StepsDir = if ($Env:STEPS_DIR) { $Env:STEPS_DIR } else { $PSScriptRoot } Write-Host "Changing to `"$Env:ACTIVATE_LICENSE_PATH`" directory." Push-Location $Env:ACTIVATE_LICENSE_PATH +# A failed license *return* is worse than a failed activate/build: it leaks +# the seat back to Unity's license pool - see mac/steps/return_license.sh's +# matching comment. This branch never checked its exit code and never +# retried before now (confirmed live via game-ci/unity-test-runner#310's +# Windows Docker matrix: "Serial number unavailable for ULF return" / +# "Connection attempt to the License Client ... failed" on the very first +# attempt, with no retry and -returnlicense missing -username/-password - +# both required for a SERIAL-mode return, same as +# mac/steps/return_license.sh and the host-mode windows/return_license.ps1 +# already pass). +$MaxAttempts = if ($Env:UNITY_LICENSE_RETRY_MAX_ATTEMPTS) { [int]$Env:UNITY_LICENSE_RETRY_MAX_ATTEMPTS } else { 4 } +$RetryDelaySeconds = 20 +$TransientPattern = 'TimeoutPolicy did not complete|Access token is unavailable|entitlement groups and 0 free entitlements|License activation has failed|No valid Unity Editor license found|License is not active|Serial number unavailable' + try { if ($Env:UNITY_LICENSING_SERVER) { # @@ -13,7 +27,27 @@ try { # Write-Host "Returning floating license: `"$($global:FLOATING_LICENSE)`"" $LicensingClientPath = Get-UnityLicensingClientExePath - & $LicensingClientPath --return-floating $global:FLOATING_LICENSE + + for ($Attempt = 1; $Attempt -le $MaxAttempts; $Attempt++) { + $ReturnOutput = & $LicensingClientPath --return-floating $global:FLOATING_LICENSE 2>&1 | Tee-Object -Variable ReturnOutputVar + $ReturnOutput | Out-Host + $ReturnExitCode = $LASTEXITCODE + $ReturnText = ($ReturnOutputVar | Out-String) + + if ($ReturnExitCode -eq 0) { break } + + if ($Attempt -lt $MaxAttempts -and $ReturnText -match $TransientPattern) { + # Exponential backoff - see mac/steps/activate.sh's matching comment. + $CurrentRetryDelay = $RetryDelaySeconds * [math]::Pow(2, $Attempt - 1) + Write-Host "Floating license return failed with a known-transient licensing error (attempt $Attempt/$MaxAttempts) - retrying in ${CurrentRetryDelay}s..." + Start-Sleep -Seconds $CurrentRetryDelay + continue + } + break + } + if ($ReturnExitCode -ne 0) { + Write-Host "##[warning] Failed to return floating license `"$($global:FLOATING_LICENSE)`" after $MaxAttempts attempts - this seat may still be held by Unity's license server." + } } elseif ($Env:UNITY_SERIAL) { # # PROFESSIONAL (SERIAL) LICENSE MODE @@ -23,10 +57,34 @@ try { # reimport its library against whatever the editor's default target # is) just to return the license (game-ci/cli#33). # + # -username/-password are required here - without them Unity has no + # way to tell this was a serial-mode activation and instead attempts a + # personal-license (ULF) return, which fails immediately with "Serial + # number unavailable for ULF return". + # $UnityExePath = Get-UnityEditorExePath $LogPath = Join-Path $Env:ACTIVATE_LICENSE_PATH 'return_license.log' - Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -returnlicense -projectPath $Env:ACTIVATE_LICENSE_PATH | Out-Host - if (Test-Path $LogPath) { Get-Content $LogPath | Out-Host } + + for ($Attempt = 1; $Attempt -le $MaxAttempts; $Attempt++) { + Invoke-UnityLaunch -ExePath $UnityExePath -logFile $LogPath -quit -returnlicense -username $Env:UNITY_EMAIL -password $Env:UNITY_PASSWORD -projectPath $Env:ACTIVATE_LICENSE_PATH | Out-Host + $ReturnExitCode = $LASTEXITCODE + $LogContent = if (Test-Path $LogPath) { Get-Content $LogPath -Raw } else { '' } + if ($LogContent) { Get-Content $LogPath | Out-Host } + + if ($ReturnExitCode -eq 0) { break } + + if ($Attempt -lt $MaxAttempts -and $LogContent -match $TransientPattern) { + # Exponential backoff - see mac/steps/activate.sh's matching comment. + $CurrentRetryDelay = $RetryDelaySeconds * [math]::Pow(2, $Attempt - 1) + Write-Host "License return failed with a known-transient licensing error (attempt $Attempt/$MaxAttempts) - retrying in ${CurrentRetryDelay}s..." + Start-Sleep -Seconds $CurrentRetryDelay + continue + } + break + } + if ($ReturnExitCode -ne 0) { + Write-Host "##[warning] Failed to return the Unity license after $MaxAttempts attempts - this seat may still be held by Unity's license server." + } } } catch { Write-Host "Could not return license: $($_.Exception.Message)"