param( # Ruta absoluta o nombre del interprete Python que lanza uvicorn. # doctor.ps1 la resuelve porque tras una instalacion fresca el PATH # de esta sesion aun no ve el Python nuevo. [string]$PythonExe = 'python' ) $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot Set-Location -LiteralPath $root # Force UTF-8 so accented messages render correctly even when launched # from the .bat via `powershell -File` (PS5.1 uses the OEM codepage by default). try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} # All console output is force-flushed so the user sees live progress even when # the launcher is invoked from a .bat-launched powershell.exe window. function Out-Line($msg, $color = $null) { if ($color) { Write-Host $msg -ForegroundColor $color } else { Write-Host $msg } [Console]::Out.Flush() } # Robust browser opener -- tries several Windows methods until one works. function Open-Browser($url) { $opened = $false try { Start-Process -FilePath $url; $opened = $true } catch {} if (-not $opened) { try { & explorer.exe $url; $opened = $true } catch {} } if (-not $opened) { try { & cmd.exe /c start "" $url; $opened = $true } catch {} } if ($opened) { Out-Line " [ok] navegador abierto -> $url" DarkGray } else { Out-Line " [warn] no se pudo abrir el navegador automaticamente" Yellow } return $opened } # Fast TCP-connect probe. Replaces Invoke-WebRequest (which on Windows # PowerShell 5.1 ignores -TimeoutSec for the connect phase and can hang # 3-15 s per failed attempt to a closed port -- making a typical port # scan silently take many minutes). function Test-PortOpen($port, $timeoutMs = 350) { $client = New-Object System.Net.Sockets.TcpClient try { $iar = $client.BeginConnect('127.0.0.1', $port, $null, $null) if (-not $iar.AsyncWaitHandle.WaitOne($timeoutMs)) { return $false } $client.EndConnect($iar) return $true } catch { return $false } finally { $client.Close() } } function Get-ServerHealth($port, $timeoutMs = 800) { try { $r = Invoke-WebRequest -Uri "http://127.0.0.1:$port/healthz" -UseBasicParsing -TimeoutSec ([Math]::Max(1, [int]($timeoutMs/1000))) if ($r.Content -match 'ok') { return $true } } catch {} return $false } function Is-PidAlive($processId) { if (-not $processId) { return $false } $p = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue return $null -ne $p } function Show-State($label, $obj) { Out-Line (" [{0}] puerto={1} pid={2} t={3:o}" -f $label, $obj.port, $obj.pid, (Get-Date)) } Out-Line "" Out-Line " [yt-scraper] iniciando servidor local..." Cyan if (-not (Test-Path '.run')) { New-Item -ItemType Directory -Path '.run' | Out-Null } # --- (1) Check for an already-running server registered in server.info --- # A bound port is ground truth that a server is live. The cmd-wrapper PID # we recorded is a weak signal (cmd /c exits as soon as python takes over), # so we DON'T require the PID to be alive -- only that the port responds. $registeredPort = $null $registeredPid = $null $liveExisting = $false if (Test-Path '.run\server.info') { $firstLine = (Get-Content '.run\server.info' -TotalCount 1) $parts = $firstLine -split '\s+' if ($parts.Count -ge 2) { $registeredPort = $parts[0] $registeredPid = $parts[1] $portBusy = Test-PortOpen ([int]$registeredPort) 350 $pidAlive = Is-PidAlive $registeredPid if ($portBusy) { # Port is bound and answering -- this IS the running server. # Even if the recorded cmd-wrapper PID died, python is alive. $liveExisting = $true } else { Out-Line " [info] Habia un registro viejo de otra sesion; limpiandolo..." Yellow Remove-Item '.run\server.info' -Force -ErrorAction SilentlyContinue if (-not $pidAlive) { # also nuke any stray python on that port, just in case Get-NetTCPConnection -State Listen -LocalPort ([int]$registeredPort) -ErrorAction SilentlyContinue | ForEach-Object { try { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } catch {} } } } } } if ($liveExisting) { Out-Line " [ok] servidor ya activo en puerto $registeredPort" Green Out-Line " URL: http://localhost:$registeredPort" Out-Line " log: .run\server.log" Out-Line " stop: stop-server.bat" Open-Browser "http://127.0.0.1:$registeredPort" | Out-Null Out-Line "" exit 0 } # --- (2) Find free ports (fast TcpListener probe, like the original) --- $candidates = @() foreach ($p in 8000..8100) { try { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $p) $listener.Start(); $listener.Stop() $candidates += $p if ($candidates.Count -ge 8) { break } } catch {} } if ($candidates.Count -eq 0) { Out-Line " [error] no se encontro ningun puerto libre (8000-8100)" Red exit 2 } # --- (3) Launch uvicorn on each candidate until one sticks --- $launched = $false $port = 0 $proc = $null foreach ($candidate in $candidates) { Remove-Item '.run\server.log' -Force -ErrorAction SilentlyContinue Remove-Item '.run\server.err.log' -Force -ErrorAction SilentlyContinue Out-Line " [...] Encendiendo el servidor (intento $candidate)..." Gray # Spawn directo de python: un wrapper cmd /c con la ruta entre comillas # dispara el quote-stripping de cmd cuando la ruta tiene espacios y el # comando muere en silencio. Start-Process con array de args no sufre eso, # y ademas server.info registra el PID real de python (no el del wrapper). try { $proc = Start-Process -FilePath $PythonExe ` -ArgumentList @('-m','uvicorn','yt_scraper.webapp.app:app','--host','127.0.0.1','--port',"$candidate",'--log-level','info') ` -WorkingDirectory $root -WindowStyle Minimized -PassThru ` -RedirectStandardOutput "$root\.run\server.log" ` -RedirectStandardError "$root\.run\server.err.log" } catch { Out-Line " [warn] Start-Process fallo en $candidate : $($_.Exception.Message)" Yellow continue } Out-Line " El servidor corre en segundo plano. Registro: .run\server.log" # Fast TCP-poll for the bound port. Bound == uvicorn listening, which # is all the user needs to know "open this URL in a browser". $boundAt = $null $bound = $false $sw = [System.Diagnostics.Stopwatch]::StartNew() for ($i = 0; $i -lt 50; $i++) { Start-Sleep -Milliseconds 200 if (Test-PortOpen $candidate 250) { $bound = $true; $boundAt = $sw.Elapsed; break } # show a tiny heartbeat every ~5 attempts if ($i -in 5,15,30,45) { Out-Line (" ... esperando puerto {0} ({1:0.0}s)" -f $candidate, $sw.Elapsed.TotalSeconds) DarkGray } } if (-not $bound) { Out-Line " [warn] Este intento no respondio; probando el siguiente..." Yellow # Show the actual reason from the logs so the user can diagnose. foreach ($logName in @('.run\server.err.log', '.run\server.log')) { if (Test-Path $logName) { $tail = Get-Content $logName -Tail 6 -ErrorAction SilentlyContinue if ($tail) { Out-Line " (ultimas lineas de $logName):" DarkGray $tail | ForEach-Object { Out-Line " $_" DarkGray } } } } try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch {} try { Get-NetTCPConnection -State Listen -LocalPort $candidate -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } } catch {} continue } # Port is bound. Reveal the URL RIGHT NOW so the user sees progress. $port = $candidate Set-Content -Path '.run\server.info' -Value "$port $($proc.Id) $(Get-Date -Format o)" -Encoding ASCII Out-Line " [ok] Servidor encendido." Green Out-Line " URL: http://localhost:$port" Out-Line " log: .run\server.log" Out-Line " stop: stop-server.bat" Out-Line " Confirmando que todo cargo bien..." DarkGray Open-Browser "http://127.0.0.1:$port" | Out-Null # Confirm /healthz (separate from TCP-bound so the URL appears first). $healthAt = $null for ($i = 0; $i -lt 25; $i++) { Start-Sleep -Milliseconds 200 if (Get-ServerHealth $port 800) { $healthAt = $sw.Elapsed; break } } if ($healthAt) { Out-Line " [ok] /healthz OK (t=$([int]$healthAt.TotalSeconds)s)" Green } else { Out-Line " [warn] El servidor abrio pero tardo en responder; revisa .run\server.log" Yellow } Out-Line "" $launched = $true break } if (-not $launched) { Out-Line " [error] no se pudo iniciar el servidor en ninguno de los puertos candidatos." Red if (Test-Path '.run\server.log') { Out-Line " --- ultimo log del servidor ---" Yellow Get-Content '.run\server.log' -Tail 25 | ForEach-Object { Out-Line " $_" } } else { Out-Line " instala dependencias: pip install -e `".[web]`"" Yellow } exit 3 }