# Safe agent deploy: new release, switch junction, restart 8910+worker. No nginx / no C:\yutongda. $ErrorActionPreference = 'Stop' $ReleaseId = 'agent-' + (Get-Date -Format 'yyyyMMdd-HHmmss') $Root = 'E:\wwwroot' $Release = Join-Path $Root ("agent\releases\" + $ReleaseId) $AgentCurrent = Join-Path $Root 'agent\current' $Python = 'C:\yutongda\tools\Python312\python.exe' $Zip = 'C:\Users\ytd_extdev_0903\incoming\agent-release.zip' Write-Output ("=== create dirs release=" + $ReleaseId + " ===") New-Item -ItemType Directory -Force -Path $Release, (Join-Path $Root 'logs'), (Join-Path $Root 'ops'), (Join-Path $Root 'agent\releases') | Out-Null if (-not (Test-Path $Zip)) { throw 'missing agent-release.zip' } if (-not (Test-Path $Python)) { throw 'missing python' } Write-Output '=== expand ===' Expand-Archive -Path $Zip -DestinationPath $Release -Force $AgentHome = Join-Path $Release 'agent' $HttpApp = Join-Path $AgentHome 'agent\http_app.py' if (-not (Test-Path $HttpApp)) { throw 'http_app.py not found' } Write-Output '=== env ===' $EnvFile = Join-Path $Root '.env' if (-not (Test-Path $EnvFile)) { throw 'missing E:\wwwroot\.env' } Copy-Item $EnvFile (Join-Path $Release '.env') -Force Copy-Item $EnvFile (Join-Path $AgentHome '.env') -Force Write-Output '=== venv + pip ===' $venvDir = Join-Path $AgentHome '.venv' $venvPy = Join-Path $venvDir 'Scripts\python.exe' $venvPip = Join-Path $venvDir 'Scripts\pip.exe' if (-not (Test-Path $venvPy)) { & $Python -m venv $venvDir if ($LASTEXITCODE -ne 0) { throw 'venv failed' } } # Windows Chinese locale: force UTF-8 so requirements.txt comments decode $env:PYTHONUTF8 = '1' $env:PYTHONIOENCODING = 'utf-8' $Req = Join-Path $AgentHome 'requirements.txt' & $venvPip install --default-timeout=180 -i https://pypi.tuna.tsinghua.edu.cn/simple -r $Req if ($LASTEXITCODE -ne 0) { throw 'pip install failed' } Write-Output '=== switch junction ===' if (Test-Path $AgentCurrent) { cmd /c ("rmdir `"" + $AgentCurrent + "`"") | Out-Null if (Test-Path $AgentCurrent) { Remove-Item $AgentCurrent -Recurse -Force } } cmd /c ("mklink /J `"" + $AgentCurrent + "`" `"" + $AgentHome + "`"") if (-not (Test-Path (Join-Path $AgentCurrent 'agent\http_app.py'))) { throw 'agent current missing http_app' } Write-Output '=== stop old 8910 only ===' Get-NetTCPConnection -LocalPort 8910 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue } Get-CimInstance Win32_Process -Filter "Name='python.exe'" | ForEach-Object { $cl = $_.CommandLine if ($null -ne $cl) { if (($cl -like '*wwwroot*agent*') -and ($cl -like '*worker_app*')) { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } } } Start-Sleep -Seconds 2 Write-Output '=== write run scripts ===' $runHttpPath = Join-Path $Root 'ops\run-agent-http.cmd' $runWorkerPath = Join-Path $Root 'ops\run-agent-worker.cmd' $logHttpOut = Join-Path $Root 'logs\agent-http.out.log' $logHttpErr = Join-Path $Root 'logs\agent-http.err.log' $logWorkerOut = Join-Path $Root 'logs\agent-worker.out.log' $logWorkerErr = Join-Path $Root 'logs\agent-worker.err.log' $runHttp = @( '@echo off', ('cd /d "' + $AgentCurrent + '"'), 'set YTD_ENV=test', 'set AGENT_HTTP_PORT=8910', ':loop', ('"' + $venvPy + '" -m agent.http_app >> "' + $logHttpOut + '" 2>> "' + $logHttpErr + '"'), ('echo agent exited, restart in 3s >> "' + $logHttpErr + '"'), 'timeout /t 3 /nobreak >nul', 'goto loop' ) -join "`r`n" Set-Content -Path $runHttpPath -Value $runHttp -Encoding ASCII $runWorker = @( '@echo off', ('cd /d "' + $AgentCurrent + '"'), 'set YTD_ENV=test', ':loop', ('"' + $venvPy + '" -m agent.worker_app >> "' + $logWorkerOut + '" 2>> "' + $logWorkerErr + '"'), ('echo worker exited, restart in 3s >> "' + $logWorkerErr + '"'), 'timeout /t 3 /nobreak >nul', 'goto loop' ) -join "`r`n" Set-Content -Path $runWorkerPath -Value $runWorker -Encoding ASCII Write-Output '=== start http + worker via WMI ===' $w1 = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ('cmd.exe /c "' + $runHttpPath + '"') } Write-Output ("HTTP_WMI=" + $w1.ReturnValue + " pid=" + $w1.ProcessId) $w2 = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ('cmd.exe /c "' + $runWorkerPath + '"') } Write-Output ("WORKER_WMI=" + $w2.ReturnValue + " pid=" + $w2.ProcessId) Start-Sleep -Seconds 8 try { $h = Invoke-WebRequest -Uri 'http://127.0.0.1:8910/health' -UseBasicParsing -TimeoutSec 8 Write-Output ("HEALTH=" + $h.StatusCode + " " + $h.Content) } catch { if (Test-Path $logHttpErr) { Get-Content $logHttpErr -Tail 40 } throw ('agent health failed: ' + $_) } $listenPid = 0 $listen = Get-NetTCPConnection -LocalPort 8910 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($listen) { $listenPid = $listen.OwningProcess } $prevAdmin = Join-Path $Root 'admin\current' $ptrPath = Join-Path $Root 'ops\active-release.json' if (Test-Path $ptrPath) { try { $old = Get-Content $ptrPath -Raw | ConvertFrom-Json if ($old.adminCurrent) { $prevAdmin = $old.adminCurrent } } catch {} } $ptr = @{ root = $Root releaseId = $ReleaseId releaseRoot = $Release agentHome = $AgentHome agentCurrent = $AgentCurrent adminCurrent = $prevAdmin pid = $listenPid updatedAt = (Get-Date).ToString('o') note = 'safe deploy: no nginx change' } $ptr | ConvertTo-Json | Set-Content $ptrPath -Encoding UTF8 Write-Output 'AGENT_SAFE_DEPLOY_OK'