$ErrorActionPreference = "Stop" $toolRoot = Split-Path -Parent $PSCommandPath $projectRoot = (Resolve-Path (Join-Path $toolRoot "..\..")).Path $serverPath = Join-Path $toolRoot "server.py" $reviewRoot = Join-Path $projectRoot "epub_review_sessions" $venvRoot = Join-Path $toolRoot ".venv" $venvPython = Join-Path $venvRoot "Scripts\python.exe" $requirements = Join-Path $toolRoot "requirements.txt" $versionFile = Join-Path $toolRoot "version.py" $defaultPort = 5177 $env:PYTHONIOENCODING = "utf-8" function Get-PythonCommand { $pyLauncher = Get-Command py -ErrorAction SilentlyContinue if ($pyLauncher) { return [pscustomobject]@{ Exe = $pyLauncher.Source; Args = @("-3") } } $python = Get-Command python -ErrorAction SilentlyContinue if ($python) { return [pscustomobject]@{ Exe = $python.Source; Args = @() } } throw "Python was not found. Install Python 3, then run this launcher again." } function Invoke-PythonCommand { param( [Parameter(Mandatory = $true)] [string]$Exe, [string[]]$Args = @() ) & $Exe @Args return $LASTEXITCODE } function Test-FlaskInstalled { & $venvPython -c "import importlib.util, sys; sys.exit(0 if importlib.util.find_spec('flask') else 1)" 2>$null return ($LASTEXITCODE -eq 0) } function Get-ExpectedVersion { if (!(Test-Path $versionFile)) { return "" } $content = Get-Content -Raw -Encoding UTF8 $versionFile $match = [regex]::Match($content, 'version\s*=\s*"([^"]+)"') if ($match.Success) { return $match.Groups[1].Value } return "" } function Test-LocalPortOpen { param( [Parameter(Mandatory = $true)] [int]$Port ) $client = New-Object System.Net.Sockets.TcpClient try { $async = $client.BeginConnect("127.0.0.1", $Port, $null, $null) if (!$async.AsyncWaitHandle.WaitOne(100, $false)) { return $false } $client.EndConnect($async) return $true } catch { return $false } finally { $client.Close() } } function Get-RunningEditorUrl { $expectedVersion = Get-ExpectedVersion for ($port = $defaultPort; $port -lt ($defaultPort + 100); $port++) { if (!(Test-LocalPortOpen -Port $port)) { continue } $url = "http://127.0.0.1:$port" try { $versionPayload = Invoke-RestMethod -Uri "$url/api/version" -TimeoutSec 1 -ErrorAction Stop if (!$expectedVersion -or $versionPayload.version -eq $expectedVersion) { return "http://localhost:$port" } } catch { continue } } return "" } $runningUrl = Get-RunningEditorUrl if ($runningUrl) { Write-Host "EPUB review editor is already running." Start-Process $runningUrl Write-Host $runningUrl Write-Host "review root: $reviewRoot" exit 0 } if (!(Test-Path $venvPython)) { Write-Host "Creating local Python environment..." $python = Get-PythonCommand $venvArgs = @() if ($python.Args) { $venvArgs += $python.Args } $venvArgs += @("-m", "venv", $venvRoot) if ((Invoke-PythonCommand -Exe $python.Exe -Args $venvArgs) -ne 0) { throw "Failed to create local Python environment." } } if (!(Test-FlaskInstalled)) { Write-Host "Installing review editor dependencies..." & $venvPython -m pip install -r $requirements if ($LASTEXITCODE -ne 0) { throw "Failed to install dependencies from requirements.txt." } if (!(Test-FlaskInstalled)) { throw "Dependencies were installed, but Flask is still unavailable in the local Python environment." } } Write-Host "Starting EPUB review editor..." & $venvPython $serverPath --review-root $reviewRoot --daemon if ($LASTEXITCODE -ne 0) { throw "Failed to start EPUB review editor." }