start.ps1 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. # ==============================================================================
  2. # lf_mri_platform -- Start services and GUI
  3. # Usage:
  4. # .\start.ps1 -- plug mode: Docker + GUI
  5. # .\start.ps1 -Mode real -- real mode: Docker + spectrometer + GUI
  6. # .\start.ps1 -GuiOnly -- GUI only (services already running)
  7. # .\start.ps1 -ServicesOnly -- Docker + spectrometer, no GUI
  8. # .\start.ps1 -SkipInstall -- skip venv check
  9. # .\start.ps1 -SkipSpectrometer -- skip native spectrometer even in real mode
  10. # ==============================================================================
  11. param(
  12. [ValidateSet("plug", "real")]
  13. [string]$Mode = "plug",
  14. [switch]$GuiOnly,
  15. [switch]$ServicesOnly,
  16. [switch]$SkipInstall,
  17. [switch]$SkipSpectrometer
  18. )
  19. $ErrorActionPreference = "Stop"
  20. $Root = $PSScriptRoot
  21. $GuiDir = Join-Path $Root "apps\gui"
  22. $VenvPython = Join-Path $GuiDir ".venv\Scripts\python.exe"
  23. $AppScript = Join-Path $Root "apps\gui\app.py"
  24. $EnvFile = Join-Path $Root ".env"
  25. $EnvExample = Join-Path $Root ".env.example"
  26. $LogFile = Join-Path $Root "start_log.txt"
  27. # Write all output to log file so errors are readable even if window closes
  28. Start-Transcript -Path $LogFile -Append | Out-Null
  29. Write-Host "=== start.ps1 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') Mode=$Mode ==="
  30. function Write-Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
  31. function Write-OK($msg) { Write-Host " [OK] $msg" -ForegroundColor Green }
  32. function Write-Warn($msg) { Write-Host " [!!] $msg" -ForegroundColor Yellow }
  33. function Write-Fail($msg) {
  34. Write-Host "`n[FAIL] $msg" -ForegroundColor Red
  35. Write-Host " Log: $LogFile" -ForegroundColor DarkGray
  36. try { Stop-Transcript | Out-Null } catch {}
  37. Write-Host "`n Press Enter to close..." -ForegroundColor DarkGray
  38. $null = Read-Host
  39. exit 1
  40. }
  41. # Keep window open on any unhandled error
  42. trap {
  43. Write-Host "`n[ERROR] $_" -ForegroundColor Red
  44. Write-Host " Log: $LogFile" -ForegroundColor DarkGray
  45. try { Stop-Transcript | Out-Null } catch {}
  46. Write-Host "`n Press Enter to close..." -ForegroundColor DarkGray
  47. $null = Read-Host
  48. exit 1
  49. }
  50. function Get-EnvPort($key, $default) {
  51. if (Test-Path $EnvFile) {
  52. $line = Get-Content $EnvFile | Select-String "^$key=(\d+)"
  53. if ($line) { return $line.Matches[0].Groups[1].Value }
  54. }
  55. return $default
  56. }
  57. # -- 1. Check / install GUI venv -----------------------------------------------
  58. if (-not $GuiOnly -and -not $SkipInstall) {
  59. Write-Step "Checking GUI environment"
  60. $needsInstall = $false
  61. if (-not (Test-Path $VenvPython)) {
  62. Write-Warn "Virtual environment not found -- running install"
  63. $needsInstall = $true
  64. } else {
  65. & $VenvPython -c "import PySide6" 2>$null
  66. if ($LASTEXITCODE -ne 0) {
  67. Write-Warn "GUI packages missing -- running install"
  68. $needsInstall = $true
  69. } else {
  70. Write-OK "Virtual environment ready"
  71. }
  72. }
  73. if ($needsInstall) {
  74. $installScript = Join-Path $Root "install.ps1"
  75. if (-not (Test-Path $installScript)) { Write-Fail "install.ps1 not found" }
  76. & powershell.exe -ExecutionPolicy Bypass -File $installScript
  77. if ($LASTEXITCODE -ne 0) { Write-Fail "install.ps1 failed" }
  78. }
  79. }
  80. # -- 2. Docker -----------------------------------------------------------------
  81. if (-not $GuiOnly) {
  82. Write-Step "Checking Docker"
  83. $dockerOk = $false
  84. try { & docker info *>$null; $dockerOk = $true } catch {}
  85. if (-not $dockerOk) {
  86. Write-Warn "Docker not responding -- starting Docker Desktop..."
  87. $dockerExe = "C:\Program Files\Docker\Docker\Docker Desktop.exe"
  88. if (Test-Path $dockerExe) { Start-Process $dockerExe }
  89. else { Write-Fail "Docker Desktop not found -- install from https://docker.com" }
  90. $waited = 0
  91. while ($waited -lt 60) {
  92. Start-Sleep 5
  93. $waited += 5
  94. try { & docker info *>$null; $dockerOk = $true; break } catch {}
  95. Write-Host " ... $waited/60 s" -ForegroundColor DarkGray
  96. }
  97. if (-not $dockerOk) { Write-Fail "Docker did not start in time" }
  98. }
  99. Write-OK "Docker is running"
  100. # -- 3. .env ---------------------------------------------------------------
  101. if (-not (Test-Path $EnvFile)) {
  102. if (Test-Path $EnvExample) {
  103. Copy-Item $EnvExample $EnvFile
  104. Write-Warn ".env created from .env.example"
  105. } else {
  106. Write-Warn ".env missing -- using Compose defaults"
  107. }
  108. }
  109. # -- 4. Start containers ---------------------------------------------------
  110. Write-Step "Starting Docker services (mode: $Mode)"
  111. $env:ORCHESTRATOR_MODE = $Mode
  112. $envArgs = if (Test-Path $EnvFile) { @("--env-file", $EnvFile) } else { @() }
  113. & docker compose @envArgs up -d
  114. if ($LASTEXITCODE -ne 0) { Write-Fail "docker compose up failed" }
  115. Write-OK "Containers started"
  116. # -- 5. Native spectrometer ------------------------------------------------
  117. if (-not $SkipSpectrometer) {
  118. Write-Step "Starting native spectrometer"
  119. $SpecDir = Join-Path $Root "services\spectrometer"
  120. $SpecVenv = Join-Path $SpecDir "mvenv\Scripts\python.exe"
  121. $PicoExe = Join-Path $SpecDir "bin\pico-tcp.exe"
  122. $oldPico = Get-Process -Name "pico-tcp" -ErrorAction SilentlyContinue
  123. if ($oldPico) { $oldPico | Stop-Process -Force; Write-Warn "Killed stale pico-tcp.exe" }
  124. if (-not (Test-Path $SpecVenv)) {
  125. Write-Warn "Spectrometer venv not found -- creating..."
  126. Push-Location $SpecDir
  127. & python -m venv mvenv
  128. if ($LASTEXITCODE -ne 0) { Write-Fail "Failed to create spectrometer venv" }
  129. Pop-Location
  130. Write-OK "Spectrometer venv created"
  131. } else {
  132. Write-OK "Spectrometer venv found"
  133. }
  134. # Always sync packages (catches new deps after git pull)
  135. Write-Host " Installing/updating spectrometer packages..." -ForegroundColor DarkGray
  136. Push-Location $SpecDir
  137. & $SpecVenv -m pip install -q --upgrade pip
  138. $pipOut = & $SpecVenv -m pip install -q -r requirements.txt 2>&1
  139. $pipFailed = ($LASTEXITCODE -ne 0)
  140. Pop-Location
  141. if ($pipFailed) {
  142. Write-Warn "pip install had issues:`n$pipOut"
  143. } else {
  144. Write-OK "Packages up to date"
  145. }
  146. # Migrations -- show output so errors are visible
  147. Write-Host " Running migrations..." -ForegroundColor DarkGray
  148. Push-Location $SpecDir
  149. $migrateOut = & $SpecVenv manage.py migrate --noinput 2>&1
  150. $migrateFailed = ($LASTEXITCODE -ne 0)
  151. Pop-Location
  152. if ($migrateFailed) {
  153. Write-Warn "Migration warnings:`n$migrateOut"
  154. } else {
  155. Write-OK "Migrations applied"
  156. }
  157. # pico-tcp.exe
  158. if (Test-Path $PicoExe) {
  159. Start-Process $PicoExe -WindowStyle Normal
  160. Write-OK "pico-tcp.exe started (visible window)"
  161. } else {
  162. Write-Warn "bin\pico-tcp.exe not found -- ADC proxy not started"
  163. }
  164. # Django runserver -- run via "cmd /k" so window stays open on error
  165. $specPort = Get-EnvPort "SPECTROMETER_PORT" "8000"
  166. $alreadyUp = $false
  167. try {
  168. $r = Invoke-WebRequest "http://localhost:$specPort/api/" `
  169. -UseBasicParsing -TimeoutSec 2 -ErrorAction Stop
  170. $alreadyUp = ($r.StatusCode -lt 400)
  171. } catch {}
  172. if ($alreadyUp) {
  173. Write-OK "Spectrometer already responding on port $specPort"
  174. } else {
  175. # Title makes the window easy to find in the taskbar.
  176. # /k keeps the window open even if Django crashes at startup.
  177. # --noreload disables Django's file-watcher subprocess (cleaner in cmd).
  178. $runCmd = "`"$SpecVenv`" manage.py runserver 0.0.0.0:$specPort --noreload"
  179. Start-Process "cmd.exe" `
  180. -ArgumentList "/k title LF-MRI Spectrometer && $runCmd" `
  181. -WorkingDirectory $SpecDir `
  182. -WindowStyle Normal
  183. Write-OK "Spectrometer started (window: 'LF-MRI Spectrometer', port $specPort)"
  184. }
  185. }
  186. # -- 6. Health check -------------------------------------------------------
  187. Write-Step "Waiting for services to become healthy"
  188. $checkSpec = -not $SkipSpectrometer
  189. $services = @(
  190. @{ Name = "Orchestrator"; Url = "http://localhost:$(Get-EnvPort 'ORCHESTRATOR_PORT' '1717')/health"; Required = $true },
  191. @{ Name = "Seq-Interp"; Url = "http://localhost:$(Get-EnvPort 'SEQ_INTERP_PORT' '7475')/health"; Required = $true },
  192. @{ Name = "Reconstructor"; Url = "http://localhost:$(Get-EnvPort 'RECONSTRUCTOR_PORT' '8081')/health"; Required = $true },
  193. @{ Name = "Spectroscopy"; Url = "http://localhost:$(Get-EnvPort 'SPECTROSCOPY_PORT' '8002')/health"; Required = $true },
  194. @{ Name = "Spectrometer"; Url = "http://localhost:$(Get-EnvPort 'SPECTROMETER_PORT' '8000')/api/"; Required = $checkSpec; AllowedCodes = @(200,301,302,401,403) }
  195. )
  196. $maxWait = 120; $interval = 3; $elapsed = 0
  197. # Helper: returns $true if the TCP port is accepting connections.
  198. # Using TCP instead of HTTP so HTTP 4xx / auth errors don't cause false negatives.
  199. function Test-ServiceUp($svc) {
  200. try {
  201. $uri = [System.Uri]$svc.Url
  202. $port = if ($uri.Port -gt 0) { $uri.Port } else { 80 }
  203. $tcp = New-Object System.Net.Sockets.TcpClient
  204. $ar = $tcp.BeginConnect($uri.Host, $port, $null, $null)
  205. $ok = $ar.AsyncWaitHandle.WaitOne(1500, $false)
  206. try { $tcp.Close() } catch {}
  207. return $ok
  208. } catch {
  209. return $false
  210. }
  211. }
  212. while ($elapsed -lt $maxWait) {
  213. $pending = @()
  214. foreach ($svc in $services) {
  215. if (-not $svc.Required) { continue }
  216. if (-not (Test-ServiceUp $svc)) { $pending += $svc.Name }
  217. }
  218. if ($pending.Count -eq 0) { Write-OK "All required services healthy"; break }
  219. Write-Host (" ... {0}/{1} s waiting: {2}" -f $elapsed, $maxWait, ($pending -join ", ")) -ForegroundColor DarkGray
  220. Start-Sleep $interval
  221. $elapsed += $interval
  222. }
  223. if ($elapsed -ge $maxWait) { Write-Warn "Some services did not respond in time -- continuing anyway" }
  224. Write-Host ""
  225. foreach ($svc in $services) {
  226. $ok = Test-ServiceUp $svc
  227. $icon = if ($ok) { "[OK]" } else { "[--]" }
  228. $color = if ($ok) { "Green" } elseif ($svc.Required) { "Yellow" } else { "DarkGray" }
  229. $suffix = if (-not $svc.Required -and -not $ok) { " (native -- start manually)" } else { "" }
  230. Write-Host (" {0,-6} {1,-16} {2}{3}" -f $icon, $svc.Name, $svc.Url, $suffix) -ForegroundColor $color
  231. }
  232. # -- 7. Write server_config.json -------------------------------------------
  233. Write-Step "Configuring GUI"
  234. $orchPort = Get-EnvPort "ORCHESTRATOR_PORT" "1717"
  235. $seqPort = Get-EnvPort "SEQ_INTERP_PORT" "7475"
  236. $spectPort = Get-EnvPort "SPECTROSCOPY_PORT" "8002"
  237. $cfgPath = Join-Path $Root "apps\gui\cfg\server_config.json"
  238. $cfgObj = @{
  239. srv_name = "srv_interp"
  240. log_dir = "log"
  241. upload_dir = "data/input"
  242. output_dir = "data/output"
  243. server_host = "0.0.0.0"
  244. server_port = [int]$seqPort
  245. orchestrator_url = "http://localhost:$orchPort"
  246. seq_interp_url = "http://localhost:$seqPort"
  247. spectroscopy_url = "http://localhost:$spectPort"
  248. mode = $Mode
  249. }
  250. $cfgObj | ConvertTo-Json -Depth 3 | Set-Content $cfgPath -Encoding utf8
  251. Write-OK "server_config.json updated (mode=$Mode orch=:$orchPort seq=:$seqPort spectro=:$spectPort)"
  252. }
  253. # -- 8. Launch GUI -------------------------------------------------------------
  254. if (-not $ServicesOnly) {
  255. Write-Step "Launching GUI"
  256. if (-not (Test-Path $VenvPython)) {
  257. Write-Warn "Venv not found -- falling back to system Python"
  258. $VenvPython = "python"
  259. }
  260. if (-not (Test-Path $AppScript)) { Write-Fail "GUI entry point not found: $AppScript" }
  261. Start-Process $VenvPython -ArgumentList "`"$AppScript`"" -WorkingDirectory $Root -WindowStyle Normal
  262. Write-OK "GUI launched"
  263. }
  264. # -- Summary -------------------------------------------------------------------
  265. Write-Host ""
  266. Write-Host "============================================================" -ForegroundColor Green
  267. Write-Host (" LF-MRI platform is running [{0} mode]" -f $Mode.ToUpper()) -ForegroundColor Green
  268. Write-Host "============================================================" -ForegroundColor Green
  269. Write-Host ""
  270. if (-not $SkipSpectrometer) {
  271. Write-Host " Stop spectrometer: close the spectrometer terminal window"
  272. Write-Host " Stop pico-tcp: services\spectrometer\autokill.bat"
  273. }
  274. Write-Host " Stop all: .\stop.ps1"
  275. Write-Host " Update code: .\update.bat"
  276. Write-Host ""
  277. Write-Host " Log saved to: $LogFile" -ForegroundColor DarkGray
  278. try { Stop-Transcript | Out-Null } catch {}
  279. Write-Host " Press Enter to close this window..." -ForegroundColor DarkGray
  280. $null = Read-Host