<# .SYNOPSIS Builds and restarts the local telesrv process with explicit runtime logs. .DESCRIPTION This helper is meant for Windows development loops. It builds the current workspace into a staging executable, stops the repo-local process currently listening on the configured MTProto port, promotes the new executable, starts it hidden, and verifies that the port is listening again. #> [CmdletBinding()] param( [string]$Listen = "0.0.0.0:2398", [string]$AdvertiseIP, [string]$PostgresDSN, [string]$PostgresContainer = "telesrv-postgres", [string]$PostgresUser = "telesrv", [string]$ExePath, [string]$LogDir, [int]$HealthTimeoutSeconds = 20, [int]$Tail = 80, [switch]$SkipBuild, [switch]$NoStart ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $RepoRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..")) if (-not $ExePath) { $ExePath = Join-Path $RepoRoot "bin\telesrv.exe" } if (-not $LogDir) { $LogDir = Join-Path $RepoRoot "logs" } $ExePath = [System.IO.Path]::GetFullPath($ExePath) $LogDir = [System.IO.Path]::GetFullPath($LogDir) $BinDir = Split-Path -Parent $ExePath $NextExePath = Join-Path $BinDir "telesrv.next.exe" function Write-Step { param([string]$Message) Write-Host "" Write-Host "== $Message ==" } function Invoke-External { param( [string]$FilePath, [string[]]$Arguments, [switch]$AllowFailure ) $oldErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Continue" try { $output = & $FilePath @Arguments 2>&1 $exitCode = $LASTEXITCODE } finally { $ErrorActionPreference = $oldErrorActionPreference } $text = ($output | ForEach-Object { $_.ToString() }) -join "`n" if ($exitCode -ne 0 -and -not $AllowFailure) { throw "$FilePath $($Arguments -join ' ') failed with exit code ${exitCode}:`n$text" } [pscustomobject]@{ ExitCode = $exitCode Output = $text } } function Get-GitOutput { param([string[]]$Arguments, [string]$Default = "unknown") $res = Invoke-External "git" $Arguments -AllowFailure if ($res.ExitCode -ne 0) { return $Default } $text = $res.Output.Trim() if ($text.Length -eq 0) { return $Default } return $text } function Get-ListenPort { param([string]$Address) if ($Address -match '^\[.+\]:(\d+)$') { return [int]$Matches[1] } if ($Address -match ':(\d+)$') { return [int]$Matches[1] } throw "Cannot parse listen port from '$Address'" } function Test-PathUnderRepo { param([string]$Path) if (-not $Path) { return $false } $full = [System.IO.Path]::GetFullPath($Path) return $full.StartsWith($RepoRoot, [System.StringComparison]::OrdinalIgnoreCase) } function Test-RepoTelesrvProcess { param( [object]$Process, [string]$ExePath ) if (-not $Process) { return $false } $path = $null try { $path = $Process.Path } catch { $path = $null } if ($path) { $fullPath = [System.IO.Path]::GetFullPath($path) $fullExePath = [System.IO.Path]::GetFullPath($ExePath) $binDir = [System.IO.Path]::GetFullPath((Split-Path -Parent $fullExePath)) $fileName = [System.IO.Path]::GetFileName($fullPath) if ($fullPath.Equals($fullExePath, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } if ($fullPath.StartsWith($binDir, [System.StringComparison]::OrdinalIgnoreCase) -and ($fileName -like "telesrv*.exe*")) { return $true } return $false } # Path can be unavailable for protected or already-exiting processes. Only # take ownership of telesrv-looking processes in that ambiguous state. return (($Process.ProcessName -eq "telesrv") -or ($Process.ProcessName -like "telesrv*")) } function Get-RepoTelesrvProcesses { param([int]$Port, [string]$ExePath) # 按 PID 去重,合并两条发现路径: # 1) 端口监听者——主路径,但 Get-NetTCPConnection 偶发返回空(曾漏判成 "no listener", # 导致旧进程没被停、promote 复制撞文件锁)。 # 2) 按进程名/路径 + 仓库 bin 下 telesrv* 可执行文件——兜底覆盖端口漏报,并能抓到 # “持有 telesrv.exe / telesrv.exe~ 文件锁但端口尚未就绪”的实例(promote 复制前必须停掉)。 $foundByPid = @{} $listenerPids = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique) foreach ($ownerPid in $listenerPids) { if (-not $ownerPid) { continue } $proc = Get-Process -Id $ownerPid -ErrorAction SilentlyContinue if (-not $proc) { continue } $path = $null try { $path = $proc.Path } catch { $path = $null } $procId = [int]$proc.Id $isRepoProcess = Test-RepoTelesrvProcess -Process $proc -ExePath $ExePath if ($isRepoProcess) { $foundByPid[$procId] = $proc } else { throw "Port $Port is held by non-repo process PID $($proc.Id) ($($proc.ProcessName)) at '$path'" } } $candidateProcesses = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -eq "telesrv" -or $_.ProcessName -like "telesrv*" }) foreach ($proc in $candidateProcesses) { $procId = [int]$proc.Id if ($foundByPid.ContainsKey($procId)) { continue } # 只接管仓库内的实例:路径在 repo/bin 下,或路径不可读但进程名看起来就是 telesrv(兜底)。 # 仓库外的同名进程(用户在别处跑的)一律不动。 $isRepoProcess = Test-RepoTelesrvProcess -Process $proc -ExePath $ExePath if ($isRepoProcess) { $foundByPid[$procId] = $proc } } return @($foundByPid.Values) } function Wait-ProcessesExited { param([int[]]$Pids) if (-not $Pids -or $Pids.Count -eq 0) { return } $deadline = (Get-Date).AddSeconds(10) while ((Get-Date) -lt $deadline) { $alive = @($Pids | Where-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue }) if ($alive.Count -eq 0) { return } Start-Sleep -Milliseconds 250 } throw "Timed out waiting for old telesrv process(es) to exit: $($Pids -join ', ')" } function Wait-PortFree { param([int]$Port, [int[]]$Pids) $deadline = (Get-Date).AddSeconds(10) while ((Get-Date) -lt $deadline) { $stillListening = @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Where-Object { $Pids -contains $_.OwningProcess }) if ($stillListening.Count -eq 0) { return } Start-Sleep -Milliseconds 250 } throw "Timed out waiting for old telesrv listener on port $Port to stop" } $ListenPort = Get-ListenPort $Listen New-Item -ItemType Directory -Force -Path $BinDir | Out-Null New-Item -ItemType Directory -Force -Path $LogDir | Out-Null Push-Location $RepoRoot try { $branch = Get-GitOutput @("branch", "--show-current") $postgresDatabase = "external" if ([string]::IsNullOrWhiteSpace($PostgresDSN)) { switch ($branch) { "main" { $postgresDatabase = "telesrv_main" } "v2" { $postgresDatabase = "telesrv_v2" } default { throw "Branch '$branch' has no implicit local PostgreSQL database. Pass -PostgresDSN explicitly." } } Write-Step "Resolve branch-isolated PostgreSQL" & (Join-Path $PSScriptRoot "ensure-local-databases.ps1") ` -PostgresContainer $PostgresContainer ` -DbUser $PostgresUser $PostgresDSN = "postgres://telesrv:telesrv@127.0.0.1:5432/$postgresDatabase`?sslmode=disable" Write-Host "[ok] branch=$branch database=$postgresDatabase" } else { Write-Step "Use explicit PostgreSQL DSN" Write-Host "[ok] branch=$branch database=explicit-override" } if (-not $SkipBuild) { Write-Step "Build telesrv" $commit = Get-GitOutput @("rev-parse", "HEAD") $dirty = Get-GitOutput @("status", "--porcelain", "--untracked-files=no") -Default "" $treeState = "clean" if ($dirty.Length -gt 0) { $treeState = "dirty" } $buildTime = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") $ldflags = "-X main.gitCommit=$commit -X main.gitBranch=$branch -X main.gitTreeState=$treeState -X main.buildTime=$buildTime" Remove-Item -LiteralPath $NextExePath -ErrorAction SilentlyContinue Invoke-External "go" @("build", "-ldflags", $ldflags, "-o", $NextExePath, ".\cmd\telesrv") | Out-Null Write-Host "[ok] built $NextExePath" Write-Host "[ok] commit=$commit branch=$branch tree=$treeState build_time=$buildTime" } Write-Step "Stop old telesrv processes" $oldProcesses = @(Get-RepoTelesrvProcesses $ListenPort $ExePath) if ($oldProcesses.Count -eq 0) { Write-Host "[ok] no existing repo-local listener on port $ListenPort" } else { $oldPids = @($oldProcesses | Select-Object -ExpandProperty Id) foreach ($proc in $oldProcesses) { Write-Host "[stop] PID $($proc.Id) $($proc.ProcessName) $($proc.Path)" Stop-Process -Id $proc.Id -Force } Wait-ProcessesExited $oldPids Wait-PortFree $ListenPort $oldPids Write-Host "[ok] stopped old listener(s): $($oldPids -join ', ')" } if (-not $SkipBuild) { Write-Step "Promote executable" # telesrv.exe 可能被外部 watcher/watchdog 抢先重生的实例占用文件锁;停掉持有者后短暂重试, # 避免直接撞 "being used by another process" 复制失败(曾因此 promote 失败)。 $promoted = $false for ($attempt = 1; $attempt -le 10; $attempt++) { try { Copy-Item -LiteralPath $NextExePath -Destination $ExePath -Force -ErrorAction Stop $promoted = $true break } catch { $holders = @(Get-RepoTelesrvProcesses $ListenPort $ExePath) foreach ($holder in $holders) { Write-Host "[stop] PID $($holder.Id) holding $ExePath; retry $attempt/10" Stop-Process -Id $holder.Id -Force -ErrorAction SilentlyContinue } Start-Sleep -Milliseconds 300 } } if (-not $promoted) { throw "Failed to promote $ExePath after retries (file kept locked; an external watcher may be respawning telesrv)" } Remove-Item -LiteralPath $NextExePath -ErrorAction SilentlyContinue Write-Host "[ok] promoted $ExePath" } elseif (-not (Test-Path -LiteralPath $ExePath)) { throw "Executable not found: $ExePath" } if ($NoStart) { Write-Host "[ok] NoStart requested; executable is ready but not running" return } Write-Step "Start telesrv" $stamp = Get-Date -Format "yyyyMMdd-HHmmss" $stdoutPath = Join-Path $LogDir "telesrv-$stamp.out.log" $stderrPath = Join-Path $LogDir "telesrv-$stamp.err.log" $env:TELESRV_LISTEN = $Listen $env:TELESRV_POSTGRES_DSN = $PostgresDSN if ($AdvertiseIP) { $env:TELESRV_ADVERTISE_IP = $AdvertiseIP } $proc = Start-Process -FilePath $ExePath ` -WorkingDirectory $RepoRoot ` -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath ` -PassThru ` -WindowStyle Hidden $deadline = (Get-Date).AddSeconds($HealthTimeoutSeconds) $listening = $false while ((Get-Date) -lt $deadline) { $proc.Refresh() if ($proc.HasExited) { $errTail = "" if (Test-Path -LiteralPath $stderrPath) { $errTail = (Get-Content -LiteralPath $stderrPath -Tail $Tail -ErrorAction SilentlyContinue) -join "`n" } throw "telesrv exited during startup with code $($proc.ExitCode):`n$errTail" } $conn = @(Get-NetTCPConnection -LocalPort $ListenPort -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.OwningProcess -eq $proc.Id }) if ($conn.Count -gt 0) { $listening = $true break } Start-Sleep -Milliseconds 250 } if (-not $listening) { throw "telesrv PID $($proc.Id) did not listen on port $ListenPort within ${HealthTimeoutSeconds}s" } Write-Host "[ok] started PID $($proc.Id), listening on $Listen" Write-Host "[ok] stdout: $stdoutPath" Write-Host "[ok] stderr: $stderrPath" if (Test-Path -LiteralPath $stderrPath) { Get-Content -LiteralPath $stderrPath -Tail $Tail } [pscustomobject]@{ Pid = $proc.Id Listen = $Listen AdvertiseIP = $env:TELESRV_ADVERTISE_IP PostgresDatabase = $postgresDatabase Exe = $ExePath Stdout = $stdoutPath Stderr = $stderrPath } } finally { Pop-Location }