From d3396b5b8f75658584d6897dc4b943483774b4b8 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 10:48:19 +0300 Subject: [PATCH] Refactor run-aspire scripts for improved cross-platform support. Replace `run-aspire.cmd` with `run-aspire.ps1` for Windows and add `run-aspire.sh` for Ubuntu. Update documentation to reflect these changes and ensure synchronization between build rules across scripts. Enhance Tailscale integration and clarify usage instructions. --- AGENTS.md | 18 ++- README.md | 19 +-- docs/design/off-queue.md | 2 +- run-aspire.cmd | 127 +--------------- run-aspire.ps1 | 132 ++++++++++++++++ run-aspire.sh | 143 ++++++++++++++++++ .../HSchool.AppHost.Tests/LaunchBuildStamp.cs | 3 +- tools/apphost-uptodate.ps1 | 4 +- 8 files changed, 304 insertions(+), 144 deletions(-) create mode 100644 run-aspire.ps1 create mode 100644 run-aspire.sh diff --git a/AGENTS.md b/AGENTS.md index 9414a9f..9dabb19 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,11 +53,13 @@ npm --prefix src/HSchool.Client run build dotnet run --project src/HSchool.AppHost ``` -`run-aspire.cmd` is the Windows entry point: it skips MSBuild when AppHost and Server dlls are -newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on PATH -it also runs `tailscale serve --bg 5173` when `tailscale` is on PATH and connected; otherwise -it warns and continues locally only. Pass `--no-tailscale` to skip Serve. Keep the script in sync -with `tools/apphost-uptodate.ps1` if the AppHost path ever moves. +`run-aspire.ps1` is the Windows entry point (`run-aspire.cmd` just forwards to it); +`run-aspire.sh` is the same on Ubuntu. They skip MSBuild when AppHost and Server dlls are +newer than C# / csproj / props. Pass `--rebuild` to force a build. When `tailscale` is on +PATH and connected they run `tailscale serve --bg 5173` and print the HTTPS client URL; +otherwise they warn and continue locally only. Pass `--no-tailscale` to skip Serve. Keep +`tools/apphost-uptodate.ps1`, `LaunchBuildStamp`, and `needs_build` in `run-aspire.sh` in +sync if the AppHost path ever moves. `dotnet run --project src/HSchool.AppHost` still compiles every time — that is for a dirty tree, not a fast relaunch. It starts the server *and* the Vite dev server and opens the Aspire @@ -201,6 +203,6 @@ only resolved a status line. Solution-wide is CI. keep going out. Opening sends one extra snapshot. A test that waits for *people* on pause will hang; a test that waits for a lesson label will not. - **`dotnet run` on the AppHost always evaluates MSBuild**, even when nothing changed. - `run-aspire.cmd` skips that with `--no-build` when AppHost and Server dlls are newer than - C# / csproj / props. `--rebuild` forces a build. Bare `dotnet run --project src/HSchool.AppHost` - still compiles. + `run-aspire.ps1` / `run-aspire.sh` skip that with `--no-build` when AppHost and Server dlls + are newer than C# / csproj / props. `--rebuild` forces a build. Bare + `dotnet run --project src/HSchool.AppHost` still compiles. diff --git a/README.md b/README.md index c8be9af..2277a51 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ school screen with a running game clock. The school itself is still empty. dotnet run --project src/HSchool.AppHost ``` -On Windows `run-aspire.cmd` does the same and can be double-clicked; it checks that the .NET SDK -and Node are on PATH first and passes any arguments through -(`run-aspire.cmd --launch-profile http`). A second launch with no C# changes skips MSBuild; -`run-aspire.cmd --rebuild` forces a build. +On Windows `run-aspire.ps1` does the same (`run-aspire.cmd` forwards to it and can be +double-clicked). On Ubuntu `./run-aspire.sh`. Both check that the .NET SDK and Node are on +PATH first and pass any arguments through (`--launch-profile http`). A second launch with no +C# changes skips MSBuild; `--rebuild` forces a build. The Aspire dashboard opens with two resources: `server` (ASP.NET Core) and `client` (Vite dev server on port **5173**). @@ -43,11 +43,12 @@ server on port **5173**). For a dev session over your tailnet, expose only the Vite client — not the game server (5180), Aspire dashboard (15180), or SwarmUI. -`run-aspire.cmd` runs `tailscale serve --bg 5173` before the AppHost when `tailscale` is on -PATH and connected. If Tailscale is missing, offline, or `serve` fails, the script prints a -warning and continues with local access only (`http://localhost:5173`). Pass `--no-tailscale` -to skip Serve entirely. You can also run the command yourself once per boot if you start with -`dotnet run --project src/HSchool.AppHost` instead. +`run-aspire.ps1` / `run-aspire.sh` run `tailscale serve --bg 5173` before the AppHost when +`tailscale` is on PATH and connected. If Tailscale is missing, offline, or `serve` fails, the script prints a +warning and continues with local access only (`http://localhost:5173`). When Serve is up, it +prints the HTTPS client URL next to the local one; the Aspire dashboard stays on localhost. +Pass `--no-tailscale` to skip Serve entirely. You can also run the command yourself once per +boot if you start with `dotnet run --project src/HSchool.AppHost` instead. Friends on the same tailnet open `https://..ts.net`. The page, REST API and game WebSocket all go through Vite’s proxy on one origin; Kestrel stays on localhost. diff --git a/docs/design/off-queue.md b/docs/design/off-queue.md index b22da8c..d8b8fe5 100644 --- a/docs/design/off-queue.md +++ b/docs/design/off-queue.md @@ -110,7 +110,7 @@ Клиентский TypeScript и JSONC модов не входы: Vite и каталог читают диск (`ContentRoot`), а не `bin`. Тестовые проекты AppHost не ссылается — их правки запуск не пересобирают. -Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `.cmd`) и в +Правила «устарел ли билд» живут в `tools/apphost-uptodate.ps1` (то, что вызывает `run-aspire.ps1`) и в `LaunchBuildStamp` (то, что проверяют тесты). Менять надо оба. ### Что не входит diff --git a/run-aspire.cmd b/run-aspire.cmd index 0bcf1ea..f7d3f04 100644 --- a/run-aspire.cmd +++ b/run-aspire.cmd @@ -1,127 +1,8 @@ @echo off -setlocal EnableDelayedExpansion - -rem Starts the whole app: game server, Vite client and the Aspire dashboard. -rem Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props. -rem run-aspire.cmd --launch-profile http -rem run-aspire.cmd --rebuild -rem run-aspire.cmd --no-tailscale skip `tailscale serve --bg 5173` - +rem Trampoline for Explorer double-click and muscle memory. The launcher is run-aspire.ps1. cd /d "%~dp0" - -where dotnet >nul 2>&1 -if errorlevel 1 ( - echo [run-aspire] dotnet SDK not found in PATH. - echo Install .NET 10: https://dotnet.microsoft.com/download - call :maybe_pause - exit /b 1 -) - -where node >nul 2>&1 -if errorlevel 1 ( - echo [run-aspire] Node.js not found in PATH - the client resource will fail to start. - echo Install Node 22.12 or newer: https://nodejs.org - echo. -) - -set "CONFIG=Debug" -set "REBUILD=0" -set "TAILSCALE_SERVE=1" -set "ARGS=" - -:parse -if "%~1"=="" goto parsed -if /I "%~1"=="--rebuild" ( - set "REBUILD=1" - shift - goto parse -) -if /I "%~1"=="--no-tailscale" ( - set "TAILSCALE_SERVE=0" - shift - goto parse -) -if /I "%~1"=="-c" if not "%~2"=="" set "CONFIG=%~2" -if /I "%~1"=="--configuration" if not "%~2"=="" set "CONFIG=%~2" -set "ARGS=!ARGS! %1" -shift -goto parse - -:parsed - -if "%REBUILD%"=="1" ( - echo [run-aspire] --rebuild: building. - goto build -) - -where powershell >nul 2>&1 -if errorlevel 1 ( - echo [run-aspire] PowerShell not found, building. - goto build -) - -powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0tools\apphost-uptodate.ps1" -RepoRoot "%CD%" -Configuration "%CONFIG%" -if errorlevel 1 ( - echo [run-aspire] sources changed or binaries missing, building. - goto build -) - -echo [run-aspire] binaries are up to date, skipping build. -goto run - -:build -echo. -dotnet build "src\HSchool.AppHost\HSchool.AppHost.csproj" -c "%CONFIG%" -if errorlevel 1 ( - echo. - echo [run-aspire] build failed. - call :maybe_pause - exit /b 1 -) -echo. - -:run -if "%TAILSCALE_SERVE%"=="1" ( - where tailscale >nul 2>&1 - if errorlevel 1 ( - echo [run-aspire] WARNING: tailscale not found in PATH. - echo Friends cannot reach the client over the tailnet. - echo Continuing with local access only ^(http://localhost:5173^). - ) else ( - tailscale status >nul 2>&1 - if errorlevel 1 ( - echo [run-aspire] WARNING: tailscale is not connected or not running. - echo Friends cannot reach the client over the tailnet. - echo Continuing with local access only ^(http://localhost:5173^). - ) else ( - echo [run-aspire] tailscale serve --bg 5173 - tailscale serve --bg 5173 - if errorlevel 1 ( - echo [run-aspire] WARNING: tailscale serve failed. - echo Friends cannot reach the client over the tailnet. - echo Continuing with local access only ^(http://localhost:5173^). - ) - ) - ) - echo. -) - -echo [run-aspire] Starting the Aspire AppHost. Press Ctrl+C to shut everything down. -echo. - -dotnet run --project "src\HSchool.AppHost\HSchool.AppHost.csproj" --no-build --no-restore !ARGS! -set "EXITCODE=!ERRORLEVEL!" - -if not "!EXITCODE!"=="0" ( - echo. - echo [run-aspire] AppHost exited with code !EXITCODE!. -) - -call :maybe_pause -exit /b !EXITCODE! - -rem Keeps the window open when the file was double-clicked from Explorer. -:maybe_pause +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0run-aspire.ps1" %* +set "EXITCODE=%ERRORLEVEL%" echo %cmdcmdline% | find /i "%~nx0" >nul if not errorlevel 1 pause -exit /b 0 +exit /b %EXITCODE% diff --git a/run-aspire.ps1 b/run-aspire.ps1 new file mode 100644 index 0000000..f12dc4b --- /dev/null +++ b/run-aspire.ps1 @@ -0,0 +1,132 @@ +# Starts the whole app: game server, Vite client and the Aspire dashboard. +# Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props. +# .\run-aspire.ps1 +# .\run-aspire.ps1 --launch-profile http +# .\run-aspire.ps1 --rebuild +# .\run-aspire.ps1 --no-tailscale + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $false + +Set-Location -LiteralPath $PSScriptRoot + +function Write-Log([string] $Message) { + Write-Host "[run-aspire] $Message" +} + +function Write-TailscaleSkipped([string] $Why) { + Write-Log "WARNING: $Why" + Write-Host ' Friends cannot reach the client over the tailnet.' + Write-Host ' Continuing with local access only (http://localhost:5173).' +} + +$configuration = 'Debug' +$rebuild = $false +$tailscaleServe = $true +$dotnetArgs = @() +$expectConfiguration = $false + +foreach ($arg in $args) { + if ($expectConfiguration) { + $configuration = $arg + $expectConfiguration = $false + continue + } + + switch ($arg) { + '--rebuild' { $rebuild = $true } + '--no-tailscale' { $tailscaleServe = $false } + { $_ -in '-c', '--configuration' } { $expectConfiguration = $true } + default { $dotnetArgs += $arg } + } +} + +if ($expectConfiguration) { + Write-Log 'missing value for -c / --configuration.' + exit 1 +} + +if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { + Write-Log 'dotnet SDK not found in PATH.' + Write-Host ' Install .NET 10: https://dotnet.microsoft.com/download' + exit 1 +} + +if (-not (Get-Command node -ErrorAction SilentlyContinue)) { + Write-Log 'Node.js not found in PATH - the client resource will fail to start.' + Write-Host ' Install Node 22.12 or newer: https://nodejs.org' + Write-Host '' +} + +if ($rebuild) { + Write-Log '--rebuild: building.' + $needBuild = $true +} else { + $stamp = Join-Path $PSScriptRoot 'tools\apphost-uptodate.ps1' + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stamp -RepoRoot $PSScriptRoot -Configuration $configuration + if ($LASTEXITCODE -eq 0) { + Write-Log 'binaries are up to date, skipping build.' + $needBuild = $false + } else { + Write-Log 'sources changed or binaries missing, building.' + $needBuild = $true + } +} + +if ($needBuild) { + Write-Host '' + & dotnet build (Join-Path $PSScriptRoot 'src\HSchool.AppHost\HSchool.AppHost.csproj') -c $configuration + if ($LASTEXITCODE -ne 0) { + Write-Host '' + Write-Log 'build failed.' + exit $LASTEXITCODE + } + Write-Host '' +} + +$clientTailscaleUrl = $null +if ($tailscaleServe) { + if (-not (Get-Command tailscale -ErrorAction SilentlyContinue)) { + Write-TailscaleSkipped 'tailscale not found in PATH.' + } else { + & tailscale status 2>$null | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-TailscaleSkipped 'tailscale is not connected or not running.' + } else { + Write-Log 'tailscale serve --bg 5173' + & tailscale serve --bg 5173 + if ($LASTEXITCODE -ne 0) { + Write-TailscaleSkipped 'tailscale serve failed.' + } else { + $line = & tailscale serve status 2>$null | Select-Object -First 1 + $token = if ($line) { ($line -split '\s+', 2)[0] } else { $null } + if ($token -like 'https://*') { + $clientTailscaleUrl = $token + } + } + } + } + Write-Host '' +} + +Write-Log 'Starting the Aspire AppHost. Press Ctrl+C to shut everything down.' +Write-Log 'Client: http://localhost:5173' +if ($clientTailscaleUrl) { + Write-Log "Client (Tailscale): $clientTailscaleUrl" +} +Write-Host '' + +$run = @( + '--project', (Join-Path $PSScriptRoot 'src\HSchool.AppHost\HSchool.AppHost.csproj') + '--no-build' + '--no-restore' +) + $dotnetArgs +& dotnet run @run +$exitCode = $LASTEXITCODE +if ($exitCode -ne 0) { + Write-Host '' + Write-Log "AppHost exited with code $exitCode." +} + +exit $exitCode diff --git a/run-aspire.sh b/run-aspire.sh new file mode 100644 index 0000000..b79fc01 --- /dev/null +++ b/run-aspire.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Starts the whole app: game server, Vite client and the Aspire dashboard. +# Skips MSBuild when AppHost and Server dlls are newer than C# / csproj / props. +# ./run-aspire.sh +# ./run-aspire.sh --launch-profile http +# ./run-aspire.sh --rebuild +# ./run-aspire.sh --no-tailscale + +set -euo pipefail +cd "$(dirname "$0")" + +log() { + echo "[run-aspire] $*" +} + +skip_tailscale() { + log "WARNING: $*" + echo " Friends cannot reach the client over the tailnet." + echo " Continuing with local access only (http://localhost:5173)." +} + +# Same rules as tools/apphost-uptodate.ps1 and LaunchBuildStamp.cs. +needs_build() { + local tfm=net10.0 + local apphost="src/HSchool.AppHost/bin/${configuration}/${tfm}/HSchool.AppHost.dll" + local server="src/HSchool.Server/bin/${configuration}/${tfm}/HSchool.Server.dll" + [[ -f "$apphost" && -f "$server" ]] || return 0 + + local oldest=$apphost + [[ "$server" -ot "$apphost" ]] && oldest=$server + + local name + for name in global.json Directory.Build.props Directory.Packages.props; do + [[ -f "$name" && "$name" -nt "$oldest" ]] && return 0 + done + + [[ -d src ]] || return 0 + + local found + found=$(find src \( -name bin -o -name obj -o -name node_modules \) -prune -o \ + -type f \( -name '*.cs' -o -name '*.csproj' -o -name '*.props' \) -newer "$oldest" -print -quit) + [[ -n "$found" ]] +} + +configuration=Debug +rebuild=0 +tailscale_serve=1 +dotnet_args=() +expect_configuration=0 + +while [[ $# -gt 0 ]]; do + if [[ $expect_configuration -eq 1 ]]; then + configuration=$1 + expect_configuration=0 + shift + continue + fi + case $1 in + --rebuild) rebuild=1 ;; + --no-tailscale) tailscale_serve=0 ;; + -c|--configuration) expect_configuration=1 ;; + *) dotnet_args+=("$1") ;; + esac + shift +done + +if [[ $expect_configuration -eq 1 ]]; then + log "missing value for -c / --configuration." + exit 1 +fi + +if ! command -v dotnet >/dev/null; then + log "dotnet SDK not found in PATH." + echo " Install .NET 10: https://dotnet.microsoft.com/download" + exit 1 +fi + +if ! command -v node >/dev/null; then + log "Node.js not found in PATH - the client resource will fail to start." + echo " Install Node 22.12 or newer: https://nodejs.org" + echo +fi + +need_build=0 +if [[ $rebuild -eq 1 ]]; then + log "--rebuild: building." + need_build=1 +elif needs_build; then + log "sources changed or binaries missing, building." + need_build=1 +else + log "binaries are up to date, skipping build." +fi + +if [[ $need_build -eq 1 ]]; then + echo + if ! dotnet build src/HSchool.AppHost/HSchool.AppHost.csproj -c "$configuration"; then + echo + log "build failed." + exit 1 + fi + echo +fi + +client_tailscale_url= +if [[ $tailscale_serve -eq 1 ]]; then + if ! command -v tailscale >/dev/null; then + skip_tailscale "tailscale not found in PATH." + elif ! tailscale status >/dev/null 2>&1; then + skip_tailscale "tailscale is not connected or not running." + else + log "tailscale serve --bg 5173" + if ! tailscale serve --bg 5173; then + skip_tailscale "tailscale serve failed." + else + line=$(tailscale serve status 2>/dev/null | head -n 1 || true) + token=${line%% *} + case $token in + https://*) client_tailscale_url=$token ;; + esac + fi + fi + echo +fi + +log "Starting the Aspire AppHost. Press Ctrl+C to shut everything down." +log "Client: http://localhost:5173" +if [[ -n "$client_tailscale_url" ]]; then + log "Client (Tailscale): $client_tailscale_url" +fi +echo + +set +e +dotnet run --project src/HSchool.AppHost/HSchool.AppHost.csproj --no-build --no-restore "${dotnet_args[@]}" +exit_code=$? +set -e + +if [[ $exit_code -ne 0 ]]; then + echo + log "AppHost exited with code ${exit_code}." +fi + +exit "$exit_code" diff --git a/tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs b/tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs index 40a1182..f4c60e9 100644 --- a/tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs +++ b/tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs @@ -1,7 +1,8 @@ namespace HSchool.AppHost.Tests; /// -/// Same rules as tools/apphost-uptodate.ps1. Keep the two in sync. +/// Same rules as tools/apphost-uptodate.ps1 and needs_build in +/// run-aspire.sh. Keep the three in sync. /// internal static class LaunchBuildStamp { diff --git a/tools/apphost-uptodate.ps1 b/tools/apphost-uptodate.ps1 index 20d08e7..f1ec2e1 100644 --- a/tools/apphost-uptodate.ps1 +++ b/tools/apphost-uptodate.ps1 @@ -1,5 +1,5 @@ -# Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs. Keep the two in sync. -# Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build. +# Same rules as tests/HSchool.AppHost.Tests/LaunchBuildStamp.cs and needs_build in run-aspire.sh. +# Keep the three in sync. Exit 0: AppHost and Server dlls are newer than inputs. Exit 1: need a build. param( [Parameter(Mandatory = $true)] [string] $RepoRoot,