param( [switch]$Headless, [switch]$NoInitialLaunch, [switch]$Once, [int]$IntervalMilliseconds = 1000, [int]$DebounceMilliseconds = 350, [int]$RetainSlots = 12 ) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..") $reloadScript = Join-Path $PSScriptRoot "dev-reload.ps1" $liveProjectPath = Join-Path $repoRoot "src\Aquarium.Engine.Live\Aquarium.Engine.Live.csproj" $watchLogPath = Join-Path $repoRoot "artifacts\dev-reload\watch.log" $liveSlotRoot = Join-Path $repoRoot "artifacts\dev-reload\live-slots" $liveReloadPointerPath = Join-Path $repoRoot "artifacts\dev-reload\live-current.txt" New-Item -ItemType Directory -Force -Path (Split-Path $watchLogPath) | Out-Null New-Item -ItemType Directory -Force -Path $liveSlotRoot | Out-Null function Write-WatchLog([string]$message) { $line = "$(Get-Date -Format o) $message" Write-Host $line Add-Content -Path $watchLogPath -Value $line } function Get-SourceFiles { $roots = @( (Join-Path $repoRoot "src"), (Join-Path $repoRoot "Aquarium.Engine.slnx"), (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 $_.Extension -in @(".cs", ".csproj", ".json", ".slnx") } } } function Get-LiveFiles { Get-SourceFiles | Where-Object { $_.FullName -like (Join-Path $repoRoot "src\Aquarium.Engine.Live\*") } } function Get-RestartFiles { Get-SourceFiles | Where-Object { $_.FullName -notlike (Join-Path $repoRoot "src\Aquarium.Engine.Live\*") } } function Get-Fingerprint($files) { $fingerprintInput = $files | 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() } } function Get-SourceFingerprint { Get-Fingerprint (Get-SourceFiles) } function Get-LiveFingerprint { Get-Fingerprint (Get-LiveFiles) } function Get-RestartFingerprint { Get-Fingerprint (Get-RestartFiles) } function Wait-StableFingerprints { $first = @{ all = Get-SourceFingerprint live = Get-LiveFingerprint restart = Get-RestartFingerprint } Start-Sleep -Milliseconds $DebounceMilliseconds $second = @{ all = Get-SourceFingerprint live = Get-LiveFingerprint restart = Get-RestartFingerprint } while ($first.all -ne $second.all) { $first = $second Start-Sleep -Milliseconds $DebounceMilliseconds $second = @{ all = Get-SourceFingerprint live = Get-LiveFingerprint restart = Get-RestartFingerprint } } return $second } function Invoke-Reload { param([string]$reason) Write-WatchLog "Reload requested: $reason" $arguments = @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $reloadScript, "-RetainSlots", "$RetainSlots" ) if ($Headless) { $arguments += "-Headless" } $process = Start-Process ` -FilePath "powershell" ` -ArgumentList $arguments ` -WorkingDirectory $repoRoot ` -Wait ` -PassThru ` -NoNewWindow if ($process.ExitCode -ne 0) { throw "dev-reload failed with exit code $($process.ExitCode)." } } 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) { throw "live runtime build failed with exit code $LASTEXITCODE." } $liveAssemblyPath = Join-Path $liveSlot "Aquarium.Engine.Live.dll" if (-not (Test-Path $liveAssemblyPath)) { throw "Expected live runtime assembly was not produced: $liveAssemblyPath" } Set-Content -Path $liveReloadPointerPath -Value $liveAssemblyPath -Encoding UTF8 Write-WatchLog "Live reload pointer updated: $liveAssemblyPath" } $lastGoodFingerprint = Wait-StableFingerprints $lastFailedFingerprint = $null if (-not $NoInitialLaunch) { try { Invoke-Reload "initial launch" $lastGoodFingerprint = Wait-StableFingerprints $lastFailedFingerprint = $null } catch { $lastFailedFingerprint = $lastGoodFingerprint.all 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 $currentFingerprint = Wait-StableFingerprints if ($currentFingerprint.all -eq $lastGoodFingerprint.all -or $currentFingerprint.all -eq $lastFailedFingerprint) { if ($Once) { break } continue } try { if ($currentFingerprint.restart -eq $lastGoodFingerprint.restart -and $currentFingerprint.live -ne $lastGoodFingerprint.live) { Invoke-LiveReload "live source change" } else { Invoke-Reload "host source change" } $lastGoodFingerprint = $currentFingerprint $lastFailedFingerprint = $null } catch { $lastFailedFingerprint = $currentFingerprint.all Write-WatchLog "Reload failed; previous good process stays alive. $($_.Exception.Message)" } if ($Once) { break } } while ($true)