2026-05-08 12:42:08 +01:00
param (
[ switch ] $Headless ,
[ switch ] $NoInitialLaunch ,
2026-05-09 00:04:45 +01:00
[ switch ] $ReopenWhenClosed ,
2026-05-08 12:42:08 +01:00
[ switch ] $Once ,
2026-05-17 23:37:20 +01:00
[ string ] $ClientProject = " src\Aquarium.Epiphany\Aquarium.Epiphany.csproj " ,
2026-05-08 12:42:08 +01:00
[ int ] $IntervalMilliseconds = 1000 ,
[ int ] $DebounceMilliseconds = 350 ,
2026-05-09 00:04:45 +01:00
[ int ] $RetainSlots = 12 ,
[ int ] $LiveReloadAckTimeoutSeconds = 6
2026-05-08 12:42:08 +01:00
)
$ErrorActionPreference = " Stop "
Set-StrictMode -Version Latest
$repoRoot = Resolve-Path ( Join-Path $PSScriptRoot " .. " )
$reloadScript = Join-Path $PSScriptRoot " dev-reload.ps1 "
2026-05-17 23:37:20 +01:00
$liveProjectPath = if ( [ System.IO.Path ] :: IsPathRooted ( $ClientProject ) ) { $ClientProject } else { Join-Path $repoRoot $ClientProject }
$liveProjectPath = ( Resolve-Path $liveProjectPath ) . Path
$liveAssemblyName = [ System.IO.Path ] :: GetFileNameWithoutExtension ( $liveProjectPath )
$clientSourceRoot = Split-Path $liveProjectPath
2026-05-08 12:42:08 +01:00
$watchLogPath = Join-Path $repoRoot " artifacts\dev-reload\watch.log "
2026-05-08 16:04:52 +01:00
$liveSlotRoot = Join-Path $repoRoot " artifacts\dev-reload\live-slots "
$liveReloadPointerPath = Join-Path $repoRoot " artifacts\dev-reload\live-current.txt "
2026-05-08 20:36:25 +01:00
$shaderPath = Join-Path $repoRoot " src\Aquarium.Engine\Render\Shaders\Aquarium.hlsl "
2026-05-15 17:53:43 +01:00
$engineShaderRoot = Join-Path $repoRoot " src\Aquarium.Engine\Render\Shaders "
2026-05-17 23:37:20 +01:00
$clientShaderRoot = Join-Path $clientSourceRoot " Shaders "
2026-05-08 23:29:56 +01:00
$visibleStatePath = Join-Path $repoRoot " artifacts\dev-reload\state.clixml "
$headlessStatePath = Join-Path $repoRoot " artifacts\dev-reload\headless-state.clixml "
$ownedProcessStatePath = if ( $Headless ) { $headlessStatePath } else { $visibleStatePath }
2026-05-08 12:42:08 +01:00
New-Item -ItemType Directory -Force -Path ( Split-Path $watchLogPath ) | Out-Null
2026-05-08 16:04:52 +01:00
New-Item -ItemType Directory -Force -Path $liveSlotRoot | Out-Null
2026-05-08 12:42:08 +01:00
2026-05-09 00:04:45 +01:00
if ( $Headless -and $ReopenWhenClosed ) {
throw " -ReopenWhenClosed is for the visible dev window; do not combine it with -Headless. "
}
2026-05-08 12:42:08 +01:00
function Write-WatchLog([string]$message ) {
$line = " $( Get-Date -Format o ) $message "
Write-Host $line
Add-Content -Path $watchLogPath -Value $line
}
2026-05-08 16:04:52 +01:00
function Get-SourceFiles {
2026-05-08 12:42:08 +01:00
$roots = @ (
( Join-Path $repoRoot " src " ) ,
2026-05-08 23:29:56 +01:00
( Join-Path $repoRoot " scripts " ) ,
2026-05-12 21:30:26 +01:00
( Join-Path $repoRoot " Aquarium.Engine.sln " ) ,
2026-05-08 12:42:08 +01:00
( Join-Path $repoRoot " global.json " )
)
foreach ( $root in $roots ) {
if ( -not ( Test-Path $root ) ) {
continue
}
$item = Get-Item -LiteralPath $root
if ( -not $item . PSIsContainer ) {
$item
continue
}
Get-ChildItem -LiteralPath $item . FullName -Recurse -File |
Where-Object {
$_ . FullName -notmatch " \\bin\\ " -and
$_ . FullName -notmatch " \\obj\\ " -and
2026-05-08 21:53:49 +01:00
$_ . FullName -notmatch " \\artifacts\\ " -and
(
2026-05-12 21:30:26 +01:00
$_ . Extension -in @ ( " .cs " , " .csproj " , " .json " , " .sln " , " .ps1 " ) -or
2026-05-08 21:53:49 +01:00
$_ . FullName -like ( Join-Path $repoRoot " src\Aquarium.Engine\Assets\* " )
)
2026-05-08 12:42:08 +01:00
}
}
}
2026-05-15 17:53:43 +01:00
function Get-ShaderFiles {
foreach ( $root in @ ( $engineShaderRoot , $clientShaderRoot ) ) {
if ( -not ( Test-Path $root ) ) {
continue
}
Get-ChildItem -LiteralPath $root -Recurse -File |
Where-Object { $_ . Extension -in @ ( " .hlsl " , " .hlsli " ) }
}
}
2026-05-08 16:04:52 +01:00
function Get-LiveFiles {
Get-SourceFiles | Where-Object {
2026-05-17 23:37:20 +01:00
$_ . FullName -like ( Join-Path $clientSourceRoot " * " )
2026-05-08 16:04:52 +01:00
}
}
function Get-RestartFiles {
Get-SourceFiles | Where-Object {
2026-05-17 23:37:20 +01:00
$_ . FullName -notlike ( Join-Path $clientSourceRoot " * " )
2026-05-08 16:04:52 +01:00
}
}
function Get-Fingerprint($files ) {
$fingerprintInput = $files |
2026-05-08 12:42:08 +01:00
Sort-Object FullName |
ForEach-Object {
" $( $_ . FullName ) | $( $_ . Length ) | $( $_ . LastWriteTimeUtc . Ticks ) "
}
$text = [ string ] :: Join ( " `n " , $fingerprintInput )
$bytes = [ System.Text.Encoding ] :: UTF8 . GetBytes ( $text )
$sha = [ System.Security.Cryptography.SHA256 ] :: Create ( )
try {
$hashBytes = $sha . ComputeHash ( $bytes )
return -join ( $hashBytes | ForEach-Object { $_ . ToString ( " x2 " ) } )
}
finally {
$sha . Dispose ( )
}
}
2026-05-08 20:36:25 +01:00
function Get-Hash([string]$text ) {
$bytes = [ System.Text.Encoding ] :: UTF8 . GetBytes ( $text )
$sha = [ System.Security.Cryptography.SHA256 ] :: Create ( )
try {
$hashBytes = $sha . ComputeHash ( $bytes )
return -join ( $hashBytes | ForEach-Object { $_ . ToString ( " x2 " ) } )
}
finally {
$sha . Dispose ( )
}
}
function Get-AquariumFrameContractText {
if ( -not ( Test-Path $shaderPath ) ) {
return " "
}
$text = Get-Content -Raw -LiteralPath $shaderPath
$match = [ regex ] :: Match (
$text ,
" cbuffer\s+AquariumFrame\s*:\s*register\(b0\)\s*\{(?<body>.*?)\}; " ,
[ System.Text.RegularExpressions.RegexOptions ] :: Singleline )
if ( -not $match . Success ) {
return " "
}
return $match . Value
}
2026-05-08 16:04:52 +01:00
function Get-SourceFingerprint {
2026-05-08 20:36:25 +01:00
$fileFingerprint = Get-Fingerprint ( Get-SourceFiles )
$contractFingerprint = Get-Hash ( Get-AquariumFrameContractText )
Get-Hash " $fileFingerprint `n AquariumFrame: $contractFingerprint "
2026-05-08 16:04:52 +01:00
}
2026-05-15 17:53:43 +01:00
function Get-ShaderFingerprint {
Get-Fingerprint ( Get-ShaderFiles )
}
function Get-AllFingerprint {
$sourceFingerprint = Get-SourceFingerprint
$shaderFingerprint = Get-ShaderFingerprint
Get-Hash " $sourceFingerprint `n Shaders: $shaderFingerprint "
}
2026-05-08 16:04:52 +01:00
function Get-LiveFingerprint {
Get-Fingerprint ( Get-LiveFiles )
}
function Get-RestartFingerprint {
2026-05-08 20:36:25 +01:00
$fileFingerprint = Get-Fingerprint ( Get-RestartFiles )
$contractFingerprint = Get-Hash ( Get-AquariumFrameContractText )
Get-Hash " $fileFingerprint `n AquariumFrame: $contractFingerprint "
2026-05-08 16:04:52 +01:00
}
function Wait-StableFingerprints {
$first = @ {
2026-05-15 17:53:43 +01:00
all = Get-AllFingerprint
2026-05-08 16:04:52 +01:00
live = Get-LiveFingerprint
restart = Get-RestartFingerprint
2026-05-15 17:53:43 +01:00
shaders = Get-ShaderFingerprint
2026-05-08 16:04:52 +01:00
}
2026-05-08 12:42:08 +01:00
Start-Sleep -Milliseconds $DebounceMilliseconds
2026-05-08 16:04:52 +01:00
$second = @ {
2026-05-15 17:53:43 +01:00
all = Get-AllFingerprint
2026-05-08 16:04:52 +01:00
live = Get-LiveFingerprint
restart = Get-RestartFingerprint
2026-05-15 17:53:43 +01:00
shaders = Get-ShaderFingerprint
2026-05-08 16:04:52 +01:00
}
2026-05-08 12:42:08 +01:00
2026-05-08 16:04:52 +01:00
while ( $first . all -ne $second . all ) {
2026-05-08 12:42:08 +01:00
$first = $second
Start-Sleep -Milliseconds $DebounceMilliseconds
2026-05-08 16:04:52 +01:00
$second = @ {
2026-05-15 17:53:43 +01:00
all = Get-AllFingerprint
2026-05-08 16:04:52 +01:00
live = Get-LiveFingerprint
restart = Get-RestartFingerprint
2026-05-15 17:53:43 +01:00
shaders = Get-ShaderFingerprint
2026-05-08 16:04:52 +01:00
}
2026-05-08 12:42:08 +01:00
}
return $second
}
function Invoke-Reload {
param ( [ string ] $reason )
Write-WatchLog " Reload requested: $reason "
$arguments = @ (
" -NoProfile " ,
" -ExecutionPolicy " , " Bypass " ,
" -File " , $reloadScript ,
2026-05-17 23:37:20 +01:00
" -ClientProject " , $liveProjectPath ,
2026-05-08 12:42:08 +01:00
" -RetainSlots " , " $RetainSlots "
)
if ( $Headless ) {
$arguments + = " -Headless "
}
2026-05-09 00:10:24 +01:00
Push-Location $repoRoot
try {
& powershell @arguments
if ( $LASTEXITCODE -ne 0 ) {
throw " dev-reload failed with exit code $LASTEXITCODE . "
}
}
finally {
Pop-Location
2026-05-08 12:42:08 +01:00
}
}
2026-05-09 00:04:45 +01:00
function Get-OwnedProcessState {
if ( -not ( Test-Path $ownedProcessStatePath ) ) {
return $null
}
try {
return Import-Clixml -Path $ownedProcessStatePath
}
catch {
Write-WatchLog " Could not read script-owned process state. $( $_ . Exception . Message ) "
return $null
}
}
2026-05-09 00:10:24 +01:00
function Test-RecordedProcessRunning {
2026-05-09 00:04:45 +01:00
$state = Get-OwnedProcessState
if ( -not $state -or -not $state . pid -or -not $state . slot ) {
2026-05-08 23:29:56 +01:00
return $false
}
2026-05-09 00:04:45 +01:00
$process = Get-Process -Id ( [ int ] $state . pid ) -ErrorAction SilentlyContinue
if ( -not $process ) {
return $false
}
$recordedPath = [ string ] $state . slot
$commandLine = $null
2026-05-08 23:29:56 +01:00
try {
2026-05-09 00:04:45 +01:00
$commandLine = ( Get-CimInstance Win32_Process -Filter " ProcessId = $( $process . Id ) " ) . CommandLine
}
catch {
$commandLine = $null
}
2026-05-08 23:29:56 +01:00
2026-05-09 00:04:45 +01:00
return [ bool ] ( $commandLine -and $commandLine -like " * $recordedPath * " )
}
2026-05-08 23:29:56 +01:00
2026-05-09 00:10:24 +01:00
function Test-OwnedProcessRunning {
if ( $NoInitialLaunch ) {
return $true
}
return Test-RecordedProcessRunning
}
2026-05-09 00:04:45 +01:00
function Invoke-Reopen {
param ( [ string ] $reason )
2026-05-08 23:29:56 +01:00
2026-05-09 00:04:45 +01:00
if ( $Headless ) {
Invoke-Reload $reason
return
2026-05-08 23:29:56 +01:00
}
2026-05-09 00:04:45 +01:00
Write-WatchLog " Reopen requested: $reason "
$arguments = @ (
" -NoProfile " ,
" -ExecutionPolicy " , " Bypass " ,
" -File " , $reloadScript ,
" -Reopen " ,
2026-05-17 23:37:20 +01:00
" -ClientProject " , $liveProjectPath ,
2026-05-09 00:04:45 +01:00
" -RetainSlots " , " $RetainSlots "
)
2026-05-09 00:10:24 +01:00
Push-Location $repoRoot
try {
& powershell @arguments
if ( $LASTEXITCODE -ne 0 ) {
throw " dev-reload -Reopen failed with exit code $LASTEXITCODE . "
}
}
finally {
Pop-Location
2026-05-08 23:29:56 +01:00
}
}
2026-05-08 16:04:52 +01:00
function Invoke-LiveReload {
param ( [ string ] $reason )
$timestamp = Get-Date -Format " yyyyMMdd-HHmmss "
$liveSlot = Join-Path $liveSlotRoot " $timestamp - $( [ guid ] :: NewGuid ( ) . ToString ( " N " ) . Substring ( 0 , 8 ) ) "
New-Item -ItemType Directory -Force -Path $liveSlot | Out-Null
Write-WatchLog " Live reload requested: $reason "
dotnet build $liveProjectPath -c Debug -o $liveSlot
if ( $LASTEXITCODE -ne 0 ) {
2026-05-12 02:13:45 +01:00
throw " client runtime build failed with exit code $LASTEXITCODE . "
2026-05-08 16:04:52 +01:00
}
2026-05-17 23:37:20 +01:00
$liveAssemblyPath = Join-Path $liveSlot " $liveAssemblyName .dll "
2026-05-08 16:04:52 +01:00
if ( -not ( Test-Path $liveAssemblyPath ) ) {
2026-05-12 02:13:45 +01:00
throw " Expected client runtime assembly was not produced: $liveAssemblyPath "
2026-05-08 16:04:52 +01:00
}
Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8
Write-WatchLog " Live reload pointer updated: $liveAssemblyPath "
2026-05-09 00:04:45 +01:00
Wait-LiveReloadAcknowledged $liveAssemblyPath
}
2026-05-15 17:53:43 +01:00
function Copy-ShaderTree {
param (
[ string ] $SourceRoot ,
[ string ] $DestinationRoot
)
if ( -not ( Test-Path $SourceRoot ) ) {
return
}
Get-ChildItem -LiteralPath $SourceRoot -Recurse -File |
Where-Object { $_ . Extension -in @ ( " .hlsl " , " .hlsli " ) } |
ForEach-Object {
$relativePath = [ System.IO.Path ] :: GetRelativePath ( $SourceRoot , $_ . FullName )
$destinationPath = Join-Path $DestinationRoot $relativePath
$destinationDirectory = Split-Path $destinationPath
New-Item -ItemType Directory -Force -Path $destinationDirectory | Out-Null
Copy-Item -LiteralPath $_ . FullName -Destination $destinationPath -Force
}
}
function Read-LogTextAfterLength {
param (
[ string ] $Path ,
[ long ] $StartLength
)
if ( -not ( Test-Path $Path ) ) {
return " "
}
$stream = [ System.IO.File ] :: Open ( $Path , [ System.IO.FileMode ] :: Open , [ System.IO.FileAccess ] :: Read , [ System.IO.FileShare ] :: ReadWrite )
try {
if ( $StartLength -gt 0 -and $StartLength -lt $stream . Length ) {
$stream . Seek ( $StartLength , [ System.IO.SeekOrigin ] :: Begin ) | Out-Null
}
elseif ( $StartLength -ge $stream . Length ) {
return " "
}
$reader = [ System.IO.StreamReader ] :: new ( $stream )
try {
return $reader . ReadToEnd ( )
}
finally {
$reader . Dispose ( )
}
}
finally {
$stream . Dispose ( )
}
}
function Wait-ShaderHotReloadAcknowledged {
param (
[ string ] $StdoutLog ,
[ string ] $StderrLog ,
[ long ] $StdoutStartLength ,
[ long ] $StderrStartLength
)
$deadline = ( Get-Date ) . AddSeconds ( [ Math ] :: Max ( 1 , $LiveReloadAckTimeoutSeconds ) )
while ( ( Get-Date ) -lt $deadline ) {
$stdoutText = Read-LogTextAfterLength -Path $StdoutLog -StartLength $StdoutStartLength
if ( $stdoutText -match " D3D12 shader hot reload applied " ) {
Write-WatchLog " Shader hot reload acknowledged by renderer. "
return
}
$stderrText = Read-LogTextAfterLength -Path $StderrLog -StartLength $StderrStartLength
if ( $stderrText -match " D3D12 shader hot reload failed " ) {
throw " renderer reported shader hot reload failure. See $StderrLog "
}
Start-Sleep -Milliseconds 250
}
throw " shader files were copied, but renderer did not acknowledge hot reload within $LiveReloadAckTimeoutSeconds seconds. Check $StdoutLog and $StderrLog . "
}
function Invoke-ShaderHotReload {
param ( [ string ] $reason )
Write-WatchLog " Shader hot reload requested: $reason "
$state = Get-OwnedProcessState
if ( -not $state -or -not $state . slot ) {
throw " no script-owned process slot is recorded for shader hot reload. "
}
if ( -not ( Test-RecordedProcessRunning ) ) {
throw " no script-owned process is running to receive shader hot reload. "
}
$slotPath = [ string ] $state . slot
$shaderDestinationRoot = Join-Path $slotPath " Render\Shaders "
New-Item -ItemType Directory -Force -Path $shaderDestinationRoot | Out-Null
$stdoutLog = if ( $state . PSObject . Properties . Name -contains " stdoutLog " -and $state . stdoutLog ) { [ string ] $state . stdoutLog } else { $stdoutLogPath }
$stderrLog = if ( $state . PSObject . Properties . Name -contains " stderrLog " -and $state . stderrLog ) { [ string ] $state . stderrLog } else { $stderrLogPath }
$stdoutStartLength = if ( Test-Path $stdoutLog ) { ( Get-Item -LiteralPath $stdoutLog ) . Length } else { 0 }
$stderrStartLength = if ( Test-Path $stderrLog ) { ( Get-Item -LiteralPath $stderrLog ) . Length } else { 0 }
Copy-ShaderTree -SourceRoot $engineShaderRoot -DestinationRoot $shaderDestinationRoot
Copy-ShaderTree -SourceRoot $clientShaderRoot -DestinationRoot $shaderDestinationRoot
Write-WatchLog " Shader files copied into running slot: $shaderDestinationRoot "
Wait-ShaderHotReloadAcknowledged -StdoutLog $stdoutLog -StderrLog $stderrLog -StdoutStartLength $stdoutStartLength -StderrStartLength $stderrStartLength
}
2026-05-09 00:04:45 +01:00
function Wait-LiveReloadAcknowledged {
param ( [ string ] $liveAssemblyPath )
2026-05-09 00:10:24 +01:00
if ( -not ( Test-RecordedProcessRunning ) ) {
2026-05-09 00:04:45 +01:00
throw " live reload pointer was updated, but no script-owned process is running to load it. "
}
$state = Get-OwnedProcessState
if ( -not $state -or -not $state . stdoutLog ) {
Write-WatchLog " Live reload acknowledgement skipped: no running process log is recorded. "
return
}
$stdoutLog = [ string ] $state . stdoutLog
$stderrLog = if ( $state . stderrLog ) { [ string ] $state . stderrLog } else { $null }
$timeoutSeconds = [ Math ] :: Max ( 1 , $LiveReloadAckTimeoutSeconds )
$deadline = ( Get-Date ) . AddSeconds ( $timeoutSeconds )
2026-05-12 02:13:45 +01:00
$appliedLine = " Client runtime reload applied: $liveAssemblyPath "
2026-05-09 00:04:45 +01:00
while ( ( Get-Date ) -lt $deadline ) {
if ( Test-Path $stdoutLog ) {
$stdoutTail = ( Get-Content -Path $stdoutLog -Tail 120 -ErrorAction SilentlyContinue ) -join " `n "
if ( $stdoutTail . Contains ( $appliedLine ) ) {
Write-WatchLog " Live reload acknowledged by host: $liveAssemblyPath "
return
}
}
if ( $stderrLog -and ( Test-Path $stderrLog ) ) {
$stderrTail = ( Get-Content -Path $stderrLog -Tail 120 -ErrorAction SilentlyContinue ) -join " `n "
2026-05-12 02:13:45 +01:00
if ( $stderrTail -match " Client runtime reload failed " ) {
throw " host reported client runtime reload failure. See $stderrLog "
2026-05-09 00:04:45 +01:00
}
}
Start-Sleep -Milliseconds 250
}
throw " live reload pointer was updated, but host did not acknowledge loading $liveAssemblyPath within $timeoutSeconds seconds. Check $stdoutLog and $stderrLog . "
2026-05-08 16:04:52 +01:00
}
$lastGoodFingerprint = Wait-StableFingerprints
2026-05-08 12:42:08 +01:00
$lastFailedFingerprint = $null
if ( -not $NoInitialLaunch ) {
try {
Invoke-Reload " initial launch "
2026-05-08 16:04:52 +01:00
$lastGoodFingerprint = Wait-StableFingerprints
2026-05-08 12:42:08 +01:00
$lastFailedFingerprint = $null
}
catch {
2026-05-08 16:04:52 +01:00
$lastFailedFingerprint = $lastGoodFingerprint . all
2026-05-08 12:42:08 +01:00
Write-WatchLog " Initial launch failed; keeping any previous good process alive. $( $_ . Exception . Message ) "
}
}
Write-WatchLog " Watching Aquarium sources. Press Ctrl+C to stop. "
do {
Start-Sleep -Milliseconds $IntervalMilliseconds
2026-05-08 16:04:52 +01:00
$currentFingerprint = Wait-StableFingerprints
2026-05-08 12:42:08 +01:00
2026-05-08 23:29:56 +01:00
if ( -not ( Test-OwnedProcessRunning ) ) {
try {
2026-05-09 00:04:45 +01:00
if ( $ReopenWhenClosed ) {
Invoke-Reopen " script-owned process is not running "
}
else {
Invoke-Reload " script-owned process is not running "
}
2026-05-08 23:29:56 +01:00
$lastGoodFingerprint = Wait-StableFingerprints
$lastFailedFingerprint = $null
$currentFingerprint = $lastGoodFingerprint
}
catch {
$lastFailedFingerprint = $currentFingerprint . all
Write-WatchLog " Relaunch failed; watcher remains alive. $( $_ . Exception . Message ) "
}
if ( $Once ) {
break
}
}
2026-05-08 16:04:52 +01:00
if ( $currentFingerprint . all -eq $lastGoodFingerprint . all -or $currentFingerprint . all -eq $lastFailedFingerprint ) {
2026-05-08 12:42:08 +01:00
if ( $Once ) {
break
}
continue
}
try {
2026-05-15 17:53:43 +01:00
if ( $currentFingerprint . restart -eq $lastGoodFingerprint . restart -and
$currentFingerprint . live -eq $lastGoodFingerprint . live -and
$currentFingerprint . shaders -ne $lastGoodFingerprint . shaders ) {
Invoke-ShaderHotReload " shader source change "
}
elseif ( $currentFingerprint . restart -eq $lastGoodFingerprint . restart -and $currentFingerprint . live -ne $lastGoodFingerprint . live ) {
2026-05-08 16:04:52 +01:00
Invoke-LiveReload " live source change "
2026-05-15 17:53:43 +01:00
if ( $currentFingerprint . shaders -ne $lastGoodFingerprint . shaders ) {
Invoke-ShaderHotReload " shader source change accompanying live reload "
}
2026-05-08 16:04:52 +01:00
}
else {
2026-05-08 21:53:49 +01:00
Invoke-Reload " host/core/content source change "
2026-05-08 16:04:52 +01:00
}
2026-05-08 12:42:08 +01:00
$lastGoodFingerprint = $currentFingerprint
$lastFailedFingerprint = $null
}
catch {
2026-05-08 16:04:52 +01:00
$lastFailedFingerprint = $currentFingerprint . all
2026-05-08 12:42:08 +01:00
Write-WatchLog " Reload failed; previous good process stays alive. $( $_ . Exception . Message ) "
}
if ( $Once ) {
break
}
} while ( $true )