diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58ea32a..d604205 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -291,6 +291,15 @@ jobs: DS64: ${{ github.workspace }}/ds64 run: scripts/package-linux-release.sh + - name: Verify Linux installer archive root + run: | + set -euo pipefail + roots="$(tar -tzf artifacts/linux/quasar-installer-linux.tar.gz | awk -F/ 'NF { print $1 }' | sort -u)" + if [ "$roots" != "Quasar" ]; then + echo "ERROR: expected quasar-installer-linux.tar.gz root directory to be Quasar; found: $roots" >&2 + exit 1 + fi + - name: Upload Linux archives uses: actions/upload-artifact@v4 with: @@ -430,6 +439,27 @@ jobs: DS64: ${{ github.workspace }}\ds64 run: scripts/package-windows-release.ps1 + - name: Verify Windows installer archive root + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path 'artifacts/windows/quasar-installer-windows.zip').Path) + try { + $roots = @( + $zip.Entries | + Where-Object { -not [string]::IsNullOrWhiteSpace($_.FullName) } | + ForEach-Object { ($_.FullName -replace '\\', '/').Split('/')[0] } | + Sort-Object -Unique + ) + } + finally { + $zip.Dispose() + } + if ($roots.Count -ne 1 -or $roots[0] -ne 'Quasar') { + throw "expected quasar-installer-windows.zip root directory to be Quasar; found: $($roots -join ', ')" + } + - name: Upload Windows archives uses: actions/upload-artifact@v4 with: @@ -494,7 +524,7 @@ jobs: | **Linux** (x64) | **`quasar-installer-linux.tar.gz`** | | **Windows** (x64) | **`quasar-installer-windows.zip`** | - Download the archive with **installer** in its name. It contains one top-level folder with the Quasar **Bootstrap launcher**, the install/uninstall scripts, and a default `appsettings.json`. Follow the [Linux](https://github.com/CometWorks/quasar/blob/main/Docs/LinuxDeploymentAndUpdates.md) or [Windows](https://github.com/CometWorks/quasar/blob/main/Docs/WindowsDeploymentAndUpdates.md) deployment guide to install it. The web UI is available on http://127.0.0.1:8080 by default (the web server listens on all interfaces at 0.0.0.0:8080). + Download the archive with **installer** in its name. It contains one top-level `Quasar` folder with the Quasar **Bootstrap launcher**, the install/uninstall scripts, and a default `appsettings.json`. Follow the [Linux](https://github.com/CometWorks/quasar/blob/main/Docs/LinuxDeploymentAndUpdates.md) or [Windows](https://github.com/CometWorks/quasar/blob/main/Docs/WindowsDeploymentAndUpdates.md) deployment guide to install it. The web UI is available on http://127.0.0.1:8080 by default (the web server listens on all interfaces at 0.0.0.0:8080). The other two archives (`quasar-web-linux-x64.tar.gz` and `quasar-web-win-x64.zip`) are updates that Quasar downloads and applies automatically — you never download them by hand. diff --git a/Directory.Build.props b/Directory.Build.props index cdcdb53..fb03844 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ Quasar.Agent and Magnetar.Protocol disable generated version attributes because SHA-256, not release metadata, is the deploy drift signal for the in-server DLLs. --> - 1.0.0 + 1.0.1 $(Version) $(Version) diff --git a/Docs/Configuration.md b/Docs/Configuration.md index 4999532..ec0b73d 100644 --- a/Docs/Configuration.md +++ b/Docs/Configuration.md @@ -60,11 +60,14 @@ worker** read JSON config from these locations, later ones overriding earlier: preserve this file during Bootstrap self-updates. UI-worker activation updates it from the staged, resolved `appsettings.json` so Bootstrap and the managed worker keep the same base settings. -2. The Quasar **data directory** `appsettings.json` — the recommended place for - persistent local overrides because it is never touched by updates: - - Windows: `%APPDATA%\Quasar\appsettings.json` - - Linux: `~/.config/Quasar/appsettings.json` by default for `install.sh` - systemd installs (or `$QUASAR_DATA_DIR/appsettings.json`) +2. The Quasar **data directory** `appsettings.json`. Bootstrap uses the install + root as the default data directory, so this is normally the same file as item + 1. Set `QUASAR_DATA_DIR` (or `--data-dir ` on Linux installs) to keep + persistent local overrides in a separate directory. + +When Bootstrap starts without a custom `QUASAR_DATA_DIR`, it migrates legacy +default data roots (`~/.config/Quasar` on Linux/macOS, +`%APPDATA%\Quasar` on Windows) into the install root. The shipped defaults are defined in [`Quasar/appsettings.json`](../Quasar/appsettings.json). diff --git a/Docs/LinuxDeploymentAndUpdates.md b/Docs/LinuxDeploymentAndUpdates.md index e84a28b..d9155b0 100644 --- a/Docs/LinuxDeploymentAndUpdates.md +++ b/Docs/LinuxDeploymentAndUpdates.md @@ -8,7 +8,7 @@ web UI worker. `scripts/package-linux-release.sh` produces: - `quasar-installer-linux.tar.gz` - - top-level `quasar-installer-linux/` directory + - top-level `Quasar/` directory - `Quasar` Bootstrap launcher - `install.sh` - `uninstall.sh` @@ -63,19 +63,19 @@ metadata is normalized to `major.minor.build`. ## First Start -The default systemd user service runs Bootstrap from -`~/.local/share/Quasar/Quasar serve --quiet` and sets `QUASAR_DATA_DIR` to the -user's Quasar data directory. It also sets `QUASAR_SYSTEMD_SERVICE` and -`QUASAR_SYSTEMD_SCOPE` so the web UI's **Shutdown Quasar** action can request -`systemctl --user stop quasar.service` instead of only exiting the launcher. -A machine-wide service is still available with `install.sh --system`. +The default systemd user service runs Bootstrap from the extracted install root +and sets `QUASAR_DATA_DIR` to that same directory. It also sets +`QUASAR_SYSTEMD_SERVICE` and `QUASAR_SYSTEMD_SCOPE` so the web UI's **Shutdown +Quasar** action can request `systemctl --user stop quasar.service` instead of +only exiting the launcher. A machine-wide service is still available with +`install.sh --system`. If Bootstrap has no usable `Updates/active-release.json` and no packaged `WebService/Quasar`, it downloads the latest Linux web asset from GitHub, extracts it under: ```text -~/.config/Quasar/ManagedRuntime/WebService/ +/ManagedRuntime/WebService/ ``` Then it writes `Updates/active-release.json` pointing at the managed active @@ -88,6 +88,9 @@ pointer that targets a random external build directory. Only packaged configured `QUASAR_WEB_EXE` / `QUASAR_WEB_DLL` workers are trusted. If Bootstrap finds an older active pointer that still targets `Updates/Staged/`, it migrates that release into `ManagedRuntime/WebService/` before launch. +On startup, Bootstrap also migrates a legacy default data root at +`~/.config/Quasar` into the install root unless `QUASAR_DATA_DIR` points to a +custom directory. ## UI Worker Updates @@ -103,7 +106,7 @@ a matching `SHA256SUMS` entry for the downloaded asset. Staging also resolves `appsettings.json`. Quasar uses the stored release base in the data directory (`$QUASAR_DATA_DIR/Updates/appsettings.base.json`) as the merge base, applies local values from the install directory -(`~/.local/share/Quasar` by default), and writes the resolved file into the staged worker. If the merge +(`` by default), and writes the resolved file into the staged worker. If the merge conflicts, auto-staging stops with a warning and `/settings/updates` shows a git-style conflict editor. Resolve and save the JSON there, or choose **Force release defaults** to stage the release file without local appsettings values. @@ -149,7 +152,7 @@ Bootstrap checks the primary Quasar release stream every 15 minutes by default. When it finds an actually newer `quasar-installer-linux.tar.gz` asset (semver core and prerelease compared against the running launcher's release identity), it verifies the release's `SHA256SUMS` entry, extracts the archive, strips the -single top-level installer directory, replaces the installed launcher files, +single top-level `Quasar` directory, replaces the installed launcher files, drains the UI worker, and exits with a failure code so systemd restarts the updated launcher. Existing `appsettings.json` is preserved. Bootstrap must not drain the worker for a release whose normalized version is @@ -183,19 +186,22 @@ first adds Microsoft's Debian 13 package feed with selected .NET package. ```bash -tar -xzf quasar-installer-linux.tar.gz -C /tmp -/tmp/quasar-installer-linux/install.sh # publish to ~/.local/share/Quasar and install user quasar.service -/tmp/quasar-installer-linux/install.sh --start # also start the user service immediately +mkdir -p ~/.local/share/Quasar +tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1 +~/.local/share/Quasar/install.sh # install user quasar.service +~/.local/share/Quasar/install.sh --start # also start the user service immediately ``` -`install.sh` publishes Quasar to `~/.local/share/Quasar`, creates the Quasar -data directory at `~/.config/Quasar` by default, and installs a user -`quasar.service`. Use `--system` with `sudo` for a machine-wide service, or -`--data-dir ` to place Quasar state elsewhere. The generated service sets -`HOME` and `QUASAR_DATA_DIR` explicitly so Bootstrap and the worker never fall -back to the install directory for update/runtime state. It also records the unit -name/scope in `QUASAR_SYSTEMD_SERVICE` and `QUASAR_SYSTEMD_SCOPE`; with those -set, the UI shutdown button asks systemd to stop the installed unit. The +For extracted release installers, `install.sh` uses the script directory as the +default install directory and the default Quasar data directory. Source installs +keep using `~/.local/share/Quasar` as the default install root, with state stored +there as well. Use `--system` with `sudo` for a machine-wide service, +`--install-dir ` to copy Quasar elsewhere, or `--data-dir ` to place +Quasar state elsewhere. The generated service sets `HOME` and `QUASAR_DATA_DIR` +explicitly so Bootstrap and the worker agree on the update/runtime state root. +It also records the unit name/scope in `QUASAR_SYSTEMD_SERVICE` and +`QUASAR_SYSTEMD_SCOPE`; with those set, the UI shutdown button asks systemd to +stop the installed unit. The installer enables the service but does not start or restart it unless `--start` is passed; start it later with `systemctl --user restart quasar.service`. When installing from source instead of an extracted release archive, the installer @@ -213,7 +219,7 @@ the whole Quasar service. The installer can build and install a narrow setuid root helper when the feature is needed: ```bash -/tmp/quasar-installer-linux/install.sh --install-renice-helper --no-build --no-enable +/tmp/Quasar/install.sh --install-renice-helper --no-build --no-enable ``` The helper is installed as `/usr/local/bin/quasar-renice`, accepts only Quasar's @@ -223,7 +229,7 @@ names before calling `setpriority`. ```bash ~/.local/share/Quasar/uninstall.sh # remove the user systemd service -~/.local/share/Quasar/uninstall.sh --purge # also remove ~/.local/share/Quasar +~/.local/share/Quasar/uninstall.sh --purge # also remove the install/data root ``` `uninstall.sh` runs `systemctl stop quasar.service` before disabling and removing @@ -238,7 +244,7 @@ For the web UI host/port (including how to change the listening port, default Update defaults live in `Quasar:Updates`. Packaged defaults come from the install directory, and operator overrides can live in the Quasar data directory -(`~/.config/Quasar/appsettings.json` by default for Linux systemd installs, or +(`/appsettings.json` by default for Linux systemd installs, or `QUASAR_DATA_DIR/appsettings.json` when overridden). The worker and Bootstrap both read that data-directory file on startup. diff --git a/Docs/QuasarArchitecture.md b/Docs/QuasarArchitecture.md index 74c2ba2..cfca418 100644 --- a/Docs/QuasarArchitecture.md +++ b/Docs/QuasarArchitecture.md @@ -468,8 +468,8 @@ Required model: Expected layout: - active runtime under a versioned release directory -- active managed web releases under `~/.config/Quasar/ManagedRuntime/WebService//` -- transient staged payloads under `~/.config/Quasar/Updates/Staged/` +- active managed web releases under `/ManagedRuntime/WebService//` +- transient staged payloads under `/Updates/Staged/` - stable release pointer / manifest for the currently active version - release identity from `AssemblyInformationalVersion` and the active-release pointer, not from numeric `AssemblyVersion` @@ -540,6 +540,10 @@ under Bootstrap, an admin can force activation from the UI. The worker writes a request file under `Updates/` containing the detected version and asset; Bootstrap consumes it with a watcher and runs the same checksum-verified self-update path for that requested release immediately. +Bootstrap also owns default data-root migration: if no custom `QUASAR_DATA_DIR` +is set, or it still points at the legacy default AppData path, Bootstrap moves +the legacy root into the launcher install root and passes that root to the +worker. The Updates page also shows installed managed-runtime versions independently of Quasar self-update state: Quasar UI/Bootstrap, Magnetar, and the Space Engineers @@ -551,6 +555,8 @@ the Quasar check button. Managed Magnetar is checked on startup and every hour after startup, with a separate manual Magnetar check button. The managed Dedicated Server is checked during startup readiness and can be forced through its own manual check button; the action runs SteamCMD `app_update 298740 validate`. +Quasar owns the SteamCMD process tree for these checks and terminates it if the +worker is stopping or the check is otherwise cancelled. ### Future proxy update flow @@ -878,9 +884,9 @@ As of this document: - the Analytics dashboard renders metrics as client-side uPlot canvas charts: the browser fetches compact, timeline-aligned series from a JSON HTTP endpoint (`/api/analytics/series`, backed by `AnalyticsSeriesService`, which selects the RRD consolidation tier by span — raw ≤2h, 1-minute ≤24h, 1-hour beyond — and drops empty buckets); profiler game-loop timing buckets (frame, update, physics, scripts, network, other) and extensive profiler top grids/entity types are surfaced as additional chart panels through the same endpoint via `ProfilerAnalyticsMetrics` and `ProfilerEntryAnalyticsMetrics`; the same page edits each server/agent profiler mode with user-facing labels ("Simple, low overhead" for `SafeContinuous`, "Extensive, deep detail" for `DeepContinuous`) and pushes live changes through `ServerCommandType.SetProfilerMode`; the previous inline `ProfilerSummaryCard` tables and the `blocks`/`floating-objects` scalar metrics have been removed - deep per-server profiler telemetry now exists: `Quasar.Agent` runs a continuous in-process profiler with `SafeContinuous` enabled by default, with per-server persisted `AgentProfilerMode` values and a global `Quasar:AgentProfilerMode` / `QUASAR_AGENT_PROFILER_MODE` fallback for older definitions. Safe mode uses Harmony prefix/postfix timing only for named high-level paths: frame/update, programmable-block script, physics, replication/network/session, GPS, and block-limit work. It deliberately avoids broad entity update method patching and detailed network-event hooks so the always-on default stays low overhead. Deep mode adds detailed network-event method hooks plus Magnetar-compatible Harmony IL call-site transpilers for `MySession.Update` / `UpdateComponents`, session component calls, replication simulation, entity update dispatch, parallel waits/callbacks, and Havok physics stepping internals. Runtime mode changes reconfigure Harmony patches so Safe, Deep, and Off can be selected without restarting the server. Hot-path measurements use numeric call-site ids and rolling accumulators, split main-thread vs off-thread time, and publish one-second windows with bounded top-lists for grids, scripts, entity types, system methods, physics detail, and network/replication/session work where the active patch depth can observe them. Patch failures are logged and the agent keeps the remaining profiler surface; entity call-site misses stay at high-level timing rather than adding broad method wrapping. Each `ProfilerSnapshot` rides the regular agent snapshot, is validated, and is kept in a small recent in-memory `ProfilerStoreService` ring (~720 samples per server, about 12 minutes at one snapshot per second), then surfaced on the Analytics page as game-loop timing and top grid/entity-type chart panels - Discord per-server options now include chat relay and simspeed alert rules. Discord-to-game chat is injected as `[Discord] : ` so in-game readers see the Discord sender, and `DiscordChatRelayService` suppresses the matching game-history echo before it can post back to Discord as the server/bot author. `DiscordSimSpeedAlertService` evaluates fresh raw metric samples for connected/running agents on the registry change path, sending alerts through the configured simspeed channel or the server's analytics channel. Baseline rules detect sharp sample-to-sample drops across every unseen raw sample pair and sustained low average simspeed, and the Discord page exposes thresholds, windows, cooldowns, and per-rule enable switches. `DiscordBotService` also publishes aggregate managed-server state through Discord presence: the bot status reflects unhealthy/faulted vs active vs idle server instances, and its activity text shows active/total servers, player count, and issue/warning counts. -- a unified GitHub-release-based update/publish pipeline now exists covering both Linux and Windows in a single combined release (`.github/workflows/release.yml`): each build produces `quasar-installer-linux.tar.gz` / `quasar-web-linux-x64.tar.gz` (Linux) and `quasar-installer-windows.zip` / `quasar-web-win-x64.zip` (Windows) under one tag; tag pushes and `main` publish full releases while pull requests publish draft prereleases; installer archives contain a single top-level `quasar-installer-*` directory for clean manual extraction; the release carries one combined `SHA256SUMS` covering every archive; release identity is normalized from `AssemblyInformationalVersion` and the active-release pointer (not numeric `AssemblyVersion`); four-part build tags such as `1.0.0.37` are canonical and numeric prerelease aliases such as `1.0.0-37` normalize to them; every downloaded asset is verified against `SHA256SUMS`; the UI stages web updates and queues them for explicit activation from `/settings/updates`; Bootstrap self-upgrades from the launcher stream only when an actually-newer asset appears (see [Linux Deployment and Updates](LinuxDeploymentAndUpdates.md) and [Windows Deployment and Updates](WindowsDeploymentAndUpdates.md)) +- a unified GitHub-release-based update/publish pipeline now exists covering both Linux and Windows in a single combined release (`.github/workflows/release.yml`): each build produces `quasar-installer-linux.tar.gz` / `quasar-web-linux-x64.tar.gz` (Linux) and `quasar-installer-windows.zip` / `quasar-web-win-x64.zip` (Windows) under one tag; tag pushes and `main` publish full releases while pull requests publish draft prereleases; installer archives contain a single top-level `Quasar` directory for clean manual extraction; the release carries one combined `SHA256SUMS` covering every archive; release identity is normalized from `AssemblyInformationalVersion` and the active-release pointer (not numeric `AssemblyVersion`); four-part build tags such as `1.0.0.37` are canonical and numeric prerelease aliases such as `1.0.0-37` normalize to them; every downloaded asset is verified against `SHA256SUMS`; the UI stages web updates and queues them for explicit activation from `/settings/updates`; Bootstrap self-upgrades from the launcher stream only when an actually-newer asset appears (see [Linux Deployment and Updates](LinuxDeploymentAndUpdates.md) and [Windows Deployment and Updates](WindowsDeploymentAndUpdates.md)) - `Quasar.Bootstrap` runs as the stable launcher that owns the public port on both Linux (systemd service) and Windows (Scheduled Task): it activates web releases through the `Updates/active-release.json` pointer after staged payloads are promoted into `ManagedRuntime/WebService//`, and performs worker cutover by draining the old worker and starting the managed one on the same port — a launcher, not yet a reverse proxy — so the public endpoint stays stable across the short listener gap while managed Magnetar servers keep running; on Linux, the UI shutdown action prefers `systemctl --user stop quasar.service` / `systemctl stop quasar.service` when the installed unit is detectable, falling back to the worker-written shutdown request that makes Bootstrap exit without respawning; on Linux the launcher exits with code 75 so systemd restarts it for self-upgrade; on Windows the launcher spawns a detached replacement `Quasar.exe serve --quiet` and exits 0, with the Scheduled Task restart-on-failure as the safety net -- Windows deployment exists via `install.ps1`/`uninstall.ps1`: `install.ps1` publishes to `%ProgramFiles%\Quasar` and registers a Scheduled Task (`Quasar`) that starts at boot and restarts the launcher on failure; the task runs with `QUASAR_MODE=Service` and `QUASAR_OPEN_BROWSER_ON_START=false` mirroring the Linux systemd environment +- Windows deployment exists via `install.ps1`/`uninstall.ps1`: extracted release installs default to the installer root, source installs default to `%ProgramFiles%\Quasar`, and the installer registers a Scheduled Task (`Quasar`) that starts at boot and restarts the launcher on failure; the task runs Bootstrap directly with `serve --quiet --service` - staged relaunch now persists supervisor runtime state so managed DS processes survive worker turnover - obsolete `webui/` is removed from the repository - per-server isolated app-data path handling groundwork exists diff --git a/Docs/QuickStart.md b/Docs/QuickStart.md index 5dd4fbb..556bc20 100644 --- a/Docs/QuickStart.md +++ b/Docs/QuickStart.md @@ -13,7 +13,7 @@ Grab the latest release from GitHub. Each release contains platform archives: ```bash tar -xzf quasar-installer-linux.tar.gz -cd quasar-installer-linux +cd Quasar ./Quasar serve ``` @@ -21,7 +21,7 @@ cd quasar-installer-linux ```cmd Expand-Archive quasar-installer-windows.zip -DestinationPath C:\quasar -cd C:\quasar\quasar-installer-windows +cd C:\quasar\Quasar Quasar.exe serve ``` @@ -42,14 +42,16 @@ commands needed before installing the .NET packages. **Linux — systemd** ```bash -tar -xzf quasar-installer-linux.tar.gz -C /tmp -/tmp/quasar-installer-linux/install.sh --start # installs to ~/.local/share/Quasar and starts quasar.service +mkdir -p ~/.local/share/Quasar +tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1 +~/.local/share/Quasar/install.sh --start # installs in place and starts quasar.service ``` -The Linux installer defaults to a user systemd service, stores Bootstrap under -`~/.local/share/Quasar`, creates `~/.config/Quasar`, and writes that data path to -the unit as `QUASAR_DATA_DIR`. Pass `--system` with `sudo` for a machine-wide -service, or `--data-dir ` to store Quasar state elsewhere. +The Linux installer defaults to a user systemd service, uses the extracted +folder as the install and data root, and writes that path to the unit as +`QUASAR_DATA_DIR`. Pass `--system` with `sudo` for a machine-wide service, +`--install-dir ` to copy Quasar elsewhere, or `--data-dir ` to store +Quasar state elsewhere. When Quasar is running from the installed user service, the UI **Shutdown Quasar** action requests `systemctl --user stop quasar.service` and leaves managed servers detached by default. @@ -66,7 +68,7 @@ To remove: ```bash ~/.local/share/Quasar/uninstall.sh # stop and remove the user service -~/.local/share/Quasar/uninstall.sh --purge # also delete ~/.local/share/Quasar +~/.local/share/Quasar/uninstall.sh --purge # also delete the install folder ``` The uninstall script stops `quasar.service` before removing it. @@ -79,18 +81,19 @@ For release assets, auto-update behaviour, and advanced configuration see Run from an **elevated PowerShell**: ```powershell -Expand-Archive quasar-installer-windows.zip -DestinationPath "$env:ProgramFiles\QuasarSetup" -cd "$env:ProgramFiles\QuasarSetup\quasar-installer-windows" -.\install.ps1 -Start # installs to %ProgramFiles%\Quasar and starts the task +Expand-Archive quasar-installer-windows.zip -DestinationPath C:\quasar +cd C:\quasar\Quasar +.\install.ps1 -Start # installs in place and starts the task ``` -The task starts at boot, restarts on failure, and runs as `SYSTEM` by default. -Pass `-User ` to run as a specific service account instead. +The task starts at boot, restarts on failure, and runs as the installing user by +default. Quasar state is stored in the same folder by default. Pass +`-InstallDir ` to copy Quasar elsewhere, or `-User ` to run as a +specific service account instead. To remove: ```powershell -cd "$env:ProgramFiles\Quasar" .\uninstall.ps1 # stop and remove the task .\uninstall.ps1 -Purge # also delete the install directory ``` diff --git a/Docs/Reference/Index.md b/Docs/Reference/Index.md index 5018dc4..1696cab 100644 --- a/Docs/Reference/Index.md +++ b/Docs/Reference/Index.md @@ -25,7 +25,7 @@ Every documented source file (210 total), alphabetical by path. See the [TOC](TO | [Magnetar.Protocol/Model/PluginRuntimeInfo.cs](files/Magnetar.Protocol/Model/PluginRuntimeInfo.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Lightweight DTO describing a single loaded plugin reported in `AgentSnapshot.Plugins`. Used by the Quasar UI to display the plugin roster and its load state. | | [Magnetar.Protocol/Model/ProfilerSnapshot.cs](files/Magnetar.Protocol/Model/ProfilerSnapshot.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | DTO classes | Wire DTOs for Quasar's continuous profiler telemetry. `ProfilerSnapshot` is embedded in `AgentSnapshot` and carries one completed rolling calculation window: frame range, frame count, per-frame game-loop timing buckets, and bounded top lists for grids, programmable blocks, entities, system methods, physics, and network/replication/session work. | | [Magnetar.Protocol/Model/ServerMetrics.cs](files/Magnetar.Protocol/Model/ServerMetrics.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | DTO carrying real-time performance and status metrics for a running SE dedicated server. Embedded in every `AgentSnapshot` so the Quasar dashboard can display server health at a glance. | -| [Magnetar.Protocol/Runtime/MagnetarPaths.cs](files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class (static) | Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), overridable via the `QUASAR_DATA_DIR` environment variable. Used by the supervisor, bootstrap launcher, and other components so they agree on file layout. | +| [Magnetar.Protocol/Runtime/MagnetarPaths.cs](files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class (static) | Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root, overridable via the `QUASAR_DATA_DIR` environment variable. The library fallback is the OS application-data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), but installed Bootstrap sets `QUASAR_DATA_DIR` to the launcher install root after migrating that legacy default, so the supervisor and launcher agree on the install-root layout. | | [Magnetar.Protocol/Runtime/QuasarActiveReleasePointer.cs](files/Magnetar.Protocol/Runtime/QuasarActiveReleasePointer.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Sealed DTO persisted to `Updates/active-release.json` (path from `MagnetarPaths.GetQuasarActiveReleasePath()`). Quasar.Bootstrap reads this file to determine which release binary to launch as the Quasar supervisor. | | [Magnetar.Protocol/Runtime/QuasarReleaseVersion.cs](files/Magnetar.Protocol/Runtime/QuasarReleaseVersion.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class (static) | Shared release-version helper used by the Quasar worker and Bootstrap launcher. It reads `AssemblyInformationalVersion`, normalizes GitHub tag names, and compares release versions with prerelease-aware ordering so update checks only report a candidate when it is actually newer. | | [Magnetar.Protocol/Runtime/QuasarWebReleaseLayout.cs](files/Magnetar.Protocol/Runtime/QuasarWebReleaseLayout.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class (static) | Shared layout validator for Linux Quasar web-release payloads. It rejects staged or downloaded web archives that are missing the worker executable or static assets required for the Blazor/MudBlazor UI to load. | @@ -47,7 +47,7 @@ Every documented source file (210 total), alphabetical by path. See the [TOC](TO | [Quasar.Agent/Quasar.Agent.csproj](files/Quasar.Agent/Quasar.Agent.csproj.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | project file | MSBuild project file for `Quasar.Agent`, a `netstandard2.0` class library (x64-only) that produces `Quasar.Agent.dll` — the Magnetar/Space Engineers plugin assembly. All game and PluginSdk references are `Private="False"` (provided by the host at runtime). Harmony is a package dependency because the agent applies profiler patches in-process. | | [Quasar.Agent/StopCommand.cs](files/Quasar.Agent/StopCommand.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | Quasar-owned PluginSdk command modules for root in-game admin lifecycle commands. `StopCommand` handles `!stop` by reporting `AdminStop` and calling `ServerControl.SaveAndQuit()`. `RestartCommand` handles `!restart` by reporting `AdminRestart` and then using save-and-quit so Quasar tracks `Restarting` and performs the relaunch. `QuitCommand` handles `!quit` by reporting `AdminStop` and calling `ServerControl.QuitWithoutSaving()` for immediate no-save shutdown. | | [Quasar.Agent/WebServiceLocator.cs](files/Quasar.Agent/WebServiceLocator.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `WebServiceLocator` resolves the base URI of the running Quasar web service. It reads the `WebServiceDiscoveryManifest` written by the supervisor, health-checks the `/api/health` endpoint, and if no healthy instance is found, attempts to launch `Quasar.Bootstrap` to start one — using a named mutex (`Quasar.Bootstrap`) to avoid concurrent spawn races. It then polls for up to 30 s for the service to become healthy. | -| [Quasar.Bootstrap/Program.cs](files/Quasar.Bootstrap/Program.cs.md) | [Quasar.Bootstrap](Modules/Quasar.Bootstrap.md) | class | Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`) and the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). | +| [Quasar.Bootstrap/Program.cs](files/Quasar.Bootstrap/Program.cs.md) | [Quasar.Bootstrap](Modules/Quasar.Bootstrap.md) | class | Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`), applies the install-root default data directory and legacy AppData migration before any path lookup, and includes the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). | | [Quasar.Bootstrap/Properties/launchSettings.json](files/Quasar.Bootstrap/Properties/launchSettings.json.md) | [Quasar.Bootstrap](Modules/Quasar.Bootstrap.md) | JSON config | Visual Studio / `dotnet run` launch settings for the Bootstrap project. Defines a single `Dev` profile that invokes `ensure-running --open-browser` under `ASPNETCORE_ENVIRONMENT=Development`, so a developer can start the full supervisor stack (and have the browser open automatically) with a single run. | | [Quasar.Bootstrap/Quasar.Bootstrap.csproj](files/Quasar.Bootstrap/Quasar.Bootstrap.csproj.md) | [Quasar.Bootstrap](Modules/Quasar.Bootstrap.md) | project file | MSBuild project file for `Quasar.Bootstrap`, a `net10.0` console executable that targets `linux-x64` and `win-x64`. RID-targeted publish restores and publishes the `Quasar` worker as a single-file sub-app into a `WebService/` subfolder. | | [Quasar/Components/App.razor](files/Quasar/Components/App.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | The root HTML document component for the Blazor Server application. It renders the full `` skeleton, wires MudBlazor, ApexCharts and app CSS, loads the Blazor WebAssembly/server JS runtime, and hosts `` and `` as the two top-level interactive components. | diff --git a/Docs/Reference/Modules/Magnetar.Protocol.md b/Docs/Reference/Modules/Magnetar.Protocol.md index 9942588..f45f4bc 100644 --- a/Docs/Reference/Modules/Magnetar.Protocol.md +++ b/Docs/Reference/Modules/Magnetar.Protocol.md @@ -29,7 +29,7 @@ Shared `netstandard2.0` contract library referenced by the Quasar supervisor, th | [Magnetar.Protocol/Model/PluginRuntimeInfo.cs](../files/Magnetar.Protocol/Model/PluginRuntimeInfo.cs.md) | class | Lightweight DTO describing a single loaded plugin reported in `AgentSnapshot.Plugins`. Used by the Quasar UI to display the plugin roster and its load state. | | [Magnetar.Protocol/Model/ProfilerSnapshot.cs](../files/Magnetar.Protocol/Model/ProfilerSnapshot.cs.md) | DTO classes | Wire DTOs for Quasar's continuous profiler telemetry. `ProfilerSnapshot` is embedded in `AgentSnapshot` and carries one completed rolling calculation window: frame range, frame count, per-frame game-loop timing buckets, and bounded top lists for grids, programmable blocks, entities, system methods, physics, and network/replication/session work. | | [Magnetar.Protocol/Model/ServerMetrics.cs](../files/Magnetar.Protocol/Model/ServerMetrics.cs.md) | class | DTO carrying real-time performance and status metrics for a running SE dedicated server. Embedded in every `AgentSnapshot` so the Quasar dashboard can display server health at a glance. | -| [Magnetar.Protocol/Runtime/MagnetarPaths.cs](../files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md) | class (static) | Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), overridable via the `QUASAR_DATA_DIR` environment variable. Used by the supervisor, bootstrap launcher, and other components so they agree on file layout. | +| [Magnetar.Protocol/Runtime/MagnetarPaths.cs](../files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md) | class (static) | Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root, overridable via the `QUASAR_DATA_DIR` environment variable. The library fallback is the OS application-data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), but installed Bootstrap sets `QUASAR_DATA_DIR` to the launcher install root after migrating that legacy default, so the supervisor and launcher agree on the install-root layout. | | [Magnetar.Protocol/Runtime/QuasarActiveReleasePointer.cs](../files/Magnetar.Protocol/Runtime/QuasarActiveReleasePointer.cs.md) | class | Sealed DTO persisted to `Updates/active-release.json` (path from `MagnetarPaths.GetQuasarActiveReleasePath()`). Quasar.Bootstrap reads this file to determine which release binary to launch as the Quasar supervisor. | | [Magnetar.Protocol/Runtime/QuasarReleaseVersion.cs](../files/Magnetar.Protocol/Runtime/QuasarReleaseVersion.cs.md) | class (static) | Shared release-version helper used by the Quasar worker and Bootstrap launcher. It reads `AssemblyInformationalVersion`, normalizes GitHub tag names, and compares release versions with prerelease-aware ordering so update checks only report a candidate when it is actually newer. | | [Magnetar.Protocol/Runtime/QuasarWebReleaseLayout.cs](../files/Magnetar.Protocol/Runtime/QuasarWebReleaseLayout.cs.md) | class (static) | Shared layout validator for Linux Quasar web-release payloads. It rejects staged or downloaded web archives that are missing the worker executable or static assets required for the Blazor/MudBlazor UI to load. | diff --git a/Docs/Reference/Modules/Quasar.Bootstrap.md b/Docs/Reference/Modules/Quasar.Bootstrap.md index 39311e0..e74490b 100644 --- a/Docs/Reference/Modules/Quasar.Bootstrap.md +++ b/Docs/Reference/Modules/Quasar.Bootstrap.md @@ -8,7 +8,7 @@ A small `net10.0` helper whose job is to make sure the Quasar web service is run | File | Kind | Summary | | --- | --- | --- | -| [Quasar.Bootstrap/Program.cs](../files/Quasar.Bootstrap/Program.cs.md) | class | Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`) and the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). | +| [Quasar.Bootstrap/Program.cs](../files/Quasar.Bootstrap/Program.cs.md) | class | Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`), applies the install-root default data directory and legacy AppData migration before any path lookup, and includes the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). | | [Quasar.Bootstrap/Properties/launchSettings.json](../files/Quasar.Bootstrap/Properties/launchSettings.json.md) | JSON config | Visual Studio / `dotnet run` launch settings for the Bootstrap project. Defines a single `Dev` profile that invokes `ensure-running --open-browser` under `ASPNETCORE_ENVIRONMENT=Development`, so a developer can start the full supervisor stack (and have the browser open automatically) with a single run. | | [Quasar.Bootstrap/Quasar.Bootstrap.csproj](../files/Quasar.Bootstrap/Quasar.Bootstrap.csproj.md) | project file | MSBuild project file for `Quasar.Bootstrap`, a `net10.0` console executable that targets `linux-x64` and `win-x64`. RID-targeted publish restores and publishes the `Quasar` worker as a single-file sub-app into a `WebService/` subfolder. | diff --git a/Docs/Reference/data/manifest.json b/Docs/Reference/data/manifest.json index ba19ba5..7526826 100644 --- a/Docs/Reference/data/manifest.json +++ b/Docs/Reference/data/manifest.json @@ -214,8 +214,8 @@ "path": "Magnetar.Protocol/Runtime/MagnetarPaths.cs", "name": "MagnetarPaths.cs", "ext": ".cs", - "size": 9431, - "sha256": "6e824b87f8b9d17ea2b5df145bcbb4039ea9c8363eacf911b056f76396a79da1", + "size": 9588, + "sha256": "72657ef5526d7d3b6ae3565d8e58ef5688bb40f4c8c34d0f2a0c590e53d8fc14", "module": "Magnetar.Protocol", "tier": 1, "status": "pending" @@ -434,8 +434,8 @@ "path": "Quasar.Bootstrap/Program.cs", "name": "Program.cs", "ext": ".cs", - "size": 86396, - "sha256": "5d5c408e229675b6d6f8b952e7ba4f8e119b68de0f246169483b0956935a5c90", + "size": 95399, + "sha256": "e6606b7369b3f9f4ab529ddd1774620981d407fffecd09ec70679e133e4a5db7", "module": "Quasar.Bootstrap", "tier": 1, "status": "pending" @@ -924,8 +924,8 @@ "path": "Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor", "name": "SteamWorkshopApiKeyDialog.razor", "ext": ".razor", - "size": 3725, - "sha256": "33dea2e47c1b007b0122bec7af1001722103e6ebc979deff4bc89ec415fcfd24", + "size": 3748, + "sha256": "d145344e73662ab3a566b75cce35f62a5b24ea096cb111d520ccefa934b03bfc", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -1734,8 +1734,8 @@ "path": "Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs", "name": "ManagedDedicatedServerRuntimeResolver.cs", "ext": ".cs", - "size": 81027, - "sha256": "72d495fc130099179afc801d59264176bcb1c9dd92d0ab432d555695e189c472", + "size": 83127, + "sha256": "ec3829ff27c94c9de350fe58d5f5aa4db84003f4af9132ce7e3b5fb01fc3364b", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" diff --git a/Docs/Reference/data/module_index.json b/Docs/Reference/data/module_index.json index 3272c00..559bef2 100644 --- a/Docs/Reference/data/module_index.json +++ b/Docs/Reference/data/module_index.json @@ -152,7 +152,7 @@ "name": "MagnetarPaths.cs", "kind": "class (static)", "tier": 1, - "summary": "Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\\Quasar` on Windows), overridable via the `QUASAR_DATA_DIR` environment variable. Used by the supervisor, bootstrap launcher, and other components so they agree on file layout." + "summary": "Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root, overridable via the `QUASAR_DATA_DIR` environment variable. The library fallback is the OS application-data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\\Quasar` on Windows), but installed Bootstrap sets `QUASAR_DATA_DIR` to the launcher install root after migrating that legacy default, so the supervisor and launcher agree on the install-root layout." }, { "path": "Magnetar.Protocol/Runtime/QuasarActiveReleasePointer.cs", @@ -310,7 +310,7 @@ "name": "Program.cs", "kind": "class", "tier": 1, - "summary": "Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`) and the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream)." + "summary": "Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`), applies the install-root default data directory and legacy AppData migration before any path lookup, and includes the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream)." }, { "path": "Quasar.Bootstrap/Properties/launchSettings.json", diff --git a/Docs/Reference/files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md b/Docs/Reference/files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md index ce4d3e3..e4e61ab 100644 --- a/Docs/Reference/files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md +++ b/Docs/Reference/files/Magnetar.Protocol/Runtime/MagnetarPaths.cs.md @@ -3,12 +3,12 @@ **Module:** Magnetar.Protocol **Kind:** class (static) **Tier:** 1 ## Summary -Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), overridable via the `QUASAR_DATA_DIR` environment variable. Used by the supervisor, bootstrap launcher, and other components so they agree on file layout. +Central, shared resolver for every on-disk path Quasar and its helpers use. All locations hang off a single Quasar data root, overridable via the `QUASAR_DATA_DIR` environment variable. The library fallback is the OS application-data root (`~/.config/Quasar` on Linux/macOS, `%APPDATA%\Quasar` on Windows), but installed Bootstrap sets `QUASAR_DATA_DIR` to the launcher install root after migrating that legacy default, so the supervisor and launcher agree on the install-root layout. ## Structure Namespace `Magnetar.Protocol.Runtime`; `public static class MagnetarPaths`. Pure path-composition helpers (no I/O); user-supplied name segments are run through `SanitizePathSegment`. -- Root: `GetQuasarDirectory()` (env override → `ApplicationData` → `AppContext.BaseDirectory`), `GetRuntimeDirectory()` (back-compat alias of the root). +- Root: `GetQuasarDirectory()` (env override → `ApplicationData` → `AppContext.BaseDirectory`), `GetRuntimeDirectory()` (back-compat alias of the root). Installed Bootstrap supplies the env override before using this resolver. - Web-service manifest: `GetWebServiceDirectory()` (the root itself), `GetWebServiceManifestPath()` → `service-manifest.json`. - Supervisor files: `GetQuasarLogDirectory()`, `GetQuasarServerLogDirectory(uniqueName)` → `Logs/Magnetars//`, `GetQuasarSupervisorStatePath()`, `GetQuasarKnownPlayersPath()` → `known-players.json`, `GetQuasarKnownPlayerSettingsPath()` → `known-player-settings.json`, `GetQuasarDiscordOptionsPath()`, `GetQuasarDataHandlingConsentPath()` → `data-handling-consent.json`, `GetQuasarBrandingPath()`, `GetQuasarBrandingDirectory()` → data-root `Branding/` asset storage, compatibility overload `GetQuasarBrandingDirectory(webRootPath)`, `GetQuasarDeathMessagesPath()`, `GetQuasarWorkshopOptionsPath()`, `GetQuasarDataProtectionKeyringDirectory()`, `GetQuasarBackupSettingsPath()` → `backup-settings.json`, `GetQuasarBackupsDirectory()` → default `Backups/` storage folder used when `Quasar:BackupDirectory` is empty. - Per-Magnetar server data (`Magnetars//`): `GetQuasarServersDirectory()`, `GetQuasarServerDirectory()`, `GetQuasarServerDedicatedServerAppDataDirectory()` (DS `-path`), `GetQuasarServerMagnetarAppDataDirectory()` (DS `-config`), `GetQuasarServerDefinitionPath()` → `server.json`, `GetQuasarServerHistoryDirectory()`, `GetQuasarServerAnalyticsPath()` → `analytics.jsonl`. @@ -24,4 +24,4 @@ Namespace `Magnetar.Protocol.Runtime`; `public static class MagnetarPaths`. Pure - `System`, `System.IO` (BCL only). ## Notes -Cross-platform by design; the `QUASAR_DATA_DIR` override is the single switch to relocate all state (e.g. containerised/multi-tenant deployments). Runtime branding assets live under this data root rather than web `wwwroot`, so custom logos/favicons survive web-service release updates. Name segments (server unique names, world template ids, managed web release versions) must be sanitized before becoming directory names — `SanitizePathSegment` is private, so callers go through the typed helpers. `GetLegacyQuasarWorldProfilesDirectory()` exists only for migration. +Cross-platform by design; the `QUASAR_DATA_DIR` override is the single switch to relocate all state (e.g. containerised/multi-tenant deployments). Runtime branding assets live under this data root rather than web `wwwroot`, so custom logos/favicons survive web-service release updates. Name segments (server unique names, world template ids, managed web release versions) must be sanitized before becoming directory names — `SanitizePathSegment` is private, so callers go through the typed helpers. `GetLegacyQuasarWorldProfilesDirectory()` exists only for world-profile migration; Bootstrap handles the older AppData-to-install-root migration before this resolver is used. diff --git a/Docs/Reference/files/Quasar.Bootstrap/Program.cs.md b/Docs/Reference/files/Quasar.Bootstrap/Program.cs.md index 4d8538e..c8130c1 100644 --- a/Docs/Reference/files/Quasar.Bootstrap/Program.cs.md +++ b/Docs/Reference/files/Quasar.Bootstrap/Program.cs.md @@ -3,16 +3,16 @@ **Module:** Quasar.Bootstrap **Kind:** class **Tier:** 1 ## Summary -Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`) and the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). +Entry point and core logic for the Quasar launcher. It implements three CLI commands (`ensure-running`, `serve`, `activate-release`), applies the install-root default data directory and legacy AppData migration before any path lookup, and includes the supporting types `BootstrapOptions` (host/port + update + preserve-servers policy from `appsettings.json`) and `LauncherCoordinator` (an `IHostedService` that supervises the Quasar worker process, watches the active-release pointer and Bootstrap update request files, downloads an initial UI worker from the UI release stream when needed, hot-reloads activated UI releases, and self-upgrades the launcher from the primary Quasar release stream). ## Structure **Namespace:** `Quasar.Bootstrap` -**Top-level types:** `Program` (internal static), `BootstrapOptions` (internal sealed), `LauncherCoordinator` (internal sealed, `IHostedService`/`IDisposable`), `LauncherForegroundOptions` (sealed record), `WorkerProcessHandle` (sealed record nested in coordinator). +**Top-level types:** `Program` (internal static), `BootstrapDataDirectoryMigration` (internal static), `BootstrapDataDirectoryMigrationResult` (readonly record struct), `BootstrapOptions` (internal sealed), `LauncherCoordinator` (internal sealed, `IHostedService`/`IDisposable`), `LauncherForegroundOptions` (sealed record), `WorkerProcessHandle` (sealed record nested in coordinator). ### `Program` (static) | Member | Description | |---|---| -| `Main(args)` | Parses flags (`--quiet`, `--open-browser`, `--force`, `--foreground`/`--console`), picks command (default `ensure-running`), decides foreground vs detached based on an attached interactive console. | +| `Main(args)` | Applies install-root data-directory migration/defaulting, parses flags (`--quiet`, `--open-browser`, `--force`, `--foreground`/`--console`), picks command (default `ensure-running`), decides foreground vs detached based on an attached interactive console. | | `EnsureRunningAsync` | Returns existing healthy service (or `--force`-kills it); acquires `Quasar.Bootstrap` named mutex; fails fast if the port is bound by a non-Quasar process; in foreground runs `ServeAsync` directly (optionally opening a browser once healthy); otherwise spawns a detached `serve` process and polls health (60×1 s). | | `ServeAsync` | Builds a `LauncherCoordinator`, starts it, blocks on Ctrl+C, then drains/stops. | | `ActivateReleaseAsync` | Writes a `QuasarActiveReleasePointer` (from `--file`/`--working-dir`/`--args`/`--version`), then `EnsureRunningAsync`. | @@ -22,6 +22,13 @@ Entry point and core logic for the Quasar launcher. It implements three CLI comm | `IsHeadless` / `TryOpenBrowser` / `TryStartBrowserCommand` | Display-server detection + cross-platform best-effort browser launch (Linux: xdg-open/gio/sensible-browser). | | `StartDetachedProcess` | Spawns the detached worker with redirected (drained) stdout/stderr so output does not bleed into the parent terminal. | +### `BootstrapDataDirectoryMigration` (static) +- Runs once at process start before `MagnetarPaths` is used. +- Treats a blank `QUASAR_DATA_DIR`, a legacy default data root (`~/.config/Quasar` / `%APPDATA%\Quasar`), or an install-root value as the default path policy. +- Uses `AppContext.BaseDirectory` as the target data root, recursively copies legacy default root contents into it, rewrites migrated `Updates/active-release.json` file/working-directory paths from the old root to the new root, removes copied legacy files/directories when possible, then sets `QUASAR_DATA_DIR` for the current process and child worker. +- Refuses the migration and falls back to the legacy root if the launcher install root is inside the legacy root, preventing recursive self-copy. +- Leaves custom `QUASAR_DATA_DIR` values untouched. + ### `BootstrapOptions` (sealed) - Reads the `Quasar` (fallback `MagnetarWeb`) config section from `appsettings.json` / `appsettings.{env}.json`, searched in `AppContext.BaseDirectory`, a `WebService` sibling, the Quasar data directory (`MagnetarPaths.GetQuasarDirectory()`), and up to 8 ancestor `Quasar/` source dirs. - Properties: `Host` (default `127.0.0.1`), `AdvertisedHost` (remaps `0.0.0.0`/`*`/`+` → `127.0.0.1`), `Port` (default 8080), `PreserveServersOnShutdown` (default true; env `QUASAR_PRESERVE_SERVERS_ON_SHUTDOWN` or `PreserveManagedServersOnShutdown`), update owner/repository/prerelease settings, `LinuxWebAssetName`/`LinuxBootstrapAssetName` (Linux defaults), `WindowsWebAssetName`/`WindowsBootstrapAssetName` (Windows defaults), computed `WebAssetName`/`BootstrapAssetName` (OS-selected), `UpdatesCheckInterval`, Bootstrap `Version`, `BaseUrl`, `ListenUrl`; const `SupervisorName = "Quasar"`. @@ -30,14 +37,14 @@ Entry point and core logic for the Quasar launcher. It implements three CLI comm | Member | Description | |---|---| | `IsReady` / `GetHealthPayload()` / `GetManifest()` | Worker liveness, health summary object (status/workerId/hostId/hostName/baseUrl/active worker version+url), and `WebServiceDiscoveryManifest`. | -| `StartAsync` | Creates dirs, downloads an initial UI worker into `ManagedRuntime/WebService/` when no packaged/active worker exists, ensures an active-release pointer exists, activates it, starts `FileSystemWatcher`s on the active pointer and Bootstrap update request file. | +| `StartAsync` | Logs any data-root migration result, creates dirs, downloads an initial UI worker into `ManagedRuntime/WebService/` when no packaged/active worker exists, ensures an active-release pointer exists, activates it, starts `FileSystemWatcher`s on the active pointer and Bootstrap update request file. | | `StopAsync` | Sets `_isStopping`, stops the pointer watcher, Bootstrap update request watcher, and launcher update monitor, drains/retires the current worker, stopping managed servers only when `!PreserveServersOnShutdown`. | | `StartBootstrapUpdateMonitor` / `RunBootstrapUpdateMonitorAsync` | Starts the self-upgrade loop on Linux and Windows when updates are enabled; checks after 30 s and then every configured update interval. | | `StartWatchingBootstrapUpdateRequests` / `QueueBootstrapUpdateRequest` | Watches `Updates/bootstrap-update-request.json`, debounces file events, reads the requested Bootstrap version/asset, deletes the request, logs the admin-triggered activation, and calls the Bootstrap self-upgrade path for that requested release immediately. | | `TryUpgradeBootstrapAsync` / `ResolveBootstrapPayloadDirectory` / `ApplyBootstrapUpdate` | Serializes self-upgrade attempts, finds either the latest allowed non-draft release containing the platform `BootstrapAssetName` (periodic monitor) or the exact version/asset from a worker request (force activation), verifies `SHA256SUMS`, extracts it, accepts either a flat launcher archive or one single top-level installer directory, skips drain/restart if the downloaded launcher is byte-identical to the installed launcher, preserves existing `appsettings.json`, replaces launcher files, drains the UI worker without stopping managed servers, then restarts: on Linux exits with code 75 so systemd restarts the updated launcher; on Windows spawns a detached `Quasar.exe serve --quiet` and exits 0 (Scheduled Task restart-on-failure is the safety net). | | `IsReleasePointerUsable` / `IsKnownReleasePath` | Validates active-release pointers. In service mode, Bootstrap rejects stale pointers to arbitrary external build directories and only trusts packaged `WebService/`, managed web releases, staged legacy updates, or explicit environment-configured worker paths. | | `ActivateCurrentReleaseAsync` | Under `_activationLock`: drains the current worker without stopping managed servers, starts the new worker, waits for `/api/health` (60 s), swaps it in, then prunes inactive managed web-release directories. | -| `StartWorkerAsync` | Copies install-directory `appsettings.json` into the worker directory, launches the worker with env vars (`QUASAR_MODE=service`, `QUASAR_LAUNCHER_TOKEN`, `QUASAR_BOOTSTRAP_VERSION`, `QUASAR_INSTALL_DIR`, `QUASAR_PRESERVE_SERVERS_ON_SHUTDOWN`, foreground console-logging), and pumps stdout/stderr in foreground. | +| `StartWorkerAsync` | Copies install-directory `appsettings.json` into the worker directory, launches the worker with env vars (`QUASAR_MODE=service`, `QUASAR_LAUNCHER_TOKEN`, `QUASAR_BOOTSTRAP_VERSION`, `QUASAR_INSTALL_DIR`, `QUASAR_DATA_DIR`, `QUASAR_PRESERVE_SERVERS_ON_SHUTDOWN`, foreground console-logging), and pumps stdout/stderr in foreground. | | `SyncInstallAppSettingsToWorker` | Keeps the managed worker's base `appsettings.json` aligned with the stable launcher/install directory before process start. | | `DrainAndRetireWorkerAsync` | POSTs `/api/internal/drain?delaySeconds=&stopServers=` with `X-Quasar-Launcher-Token`, waits for exit, force-kills on timeout. | | `HandleWorkerExited` | On unexpected worker exit (not stopping), restarts via `ActivateCurrentReleaseAsync(force: true)`. | @@ -56,5 +63,5 @@ Entry point and core logic for the Quasar launcher. It implements three CLI comm ## Notes - The `Quasar.Bootstrap` named mutex serializes spawn attempts across processes on a machine. - `IsCurrentBootstrapAssembly` / `IsCurrentBootstrapExecutable` prevent pointing the worker at the bootstrap itself; RID-targeted DLL paths are rejected when no sibling `runtimeconfig.json` exists (avoids libhostpolicy failures from the `obj/` tree). -- `PreserveServersOnShutdown` and `QUASAR_INSTALL_DIR` are propagated to the worker so the launcher and worker agree on shutdown policy and the update service can sync resolved appsettings back to the stable install directory. A worker-written `launcher-shutdown-request` file lets Bootstrap exit cleanly for full Quasar shutdown while preserving servers; a worker-written `Updates/bootstrap-update-request.json` file identifies the detected target version/asset and lets the Updates page ask Bootstrap to run launcher self-update immediately. +- `PreserveServersOnShutdown`, `QUASAR_INSTALL_DIR`, and `QUASAR_DATA_DIR` are propagated to the worker so the launcher and worker agree on shutdown policy, install root, and update/runtime state root. A worker-written `launcher-shutdown-request` file lets Bootstrap exit cleanly for full Quasar shutdown while preserving servers; a worker-written `Updates/bootstrap-update-request.json` file identifies the detected target version/asset and lets the Updates page ask Bootstrap to run launcher self-update immediately. - Initial UI worker download scans GitHub releases for the newest non-draft release containing the configured UI asset (`WebAssetName`, OS-selected), extracts it into `ManagedRuntime/WebService/`, and validates the extracted web layout before activation; periodic launcher self-upgrade scans the primary Quasar release stream for the platform `BootstrapAssetName` and compares against the normalized release identity, not raw `AssemblyVersion`. Request-file-triggered Bootstrap updates resolve the exact version/asset requested by the worker, so force activation can apply a candidate the running launcher has not selected from its own periodic stream yet. Launcher self-upgrade strips the single `quasar-installer-*` archive directory when present, while still accepting older flat launcher archives. If version metadata is stale but the installed launcher already matches the downloaded update byte-for-byte, Bootstrap logs and skips the worker drain/restart instead of repeating the same self-update every check. Both periodic and request-file-triggered Bootstrap updates share a semaphore and the same verified install path. Periodic scans honor `Quasar:Updates:IncludePrerelease`, including the data-directory override written by the Updates page after Bootstrap restarts. Service-mode active-release pointer validation prevents a previously written local `bin/Debug` worker path from overriding the installed packaged worker. diff --git a/Docs/Reference/files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md b/Docs/Reference/files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md index 78d7a58..4ef4691 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md @@ -12,7 +12,7 @@ Small MudBlazor dialog for entering or updating the Steam Web API key used serve - `string CurrentWebApiKey` — pre-populates the field when editing an existing key. - **Key UI** - Explanatory `MudText` plus an outlined info alert with a prominent bold `MudLink` to `https://steamcommunity.com/dev/apikey`. - - Platform-specific storage text: Windows uses `%APPDATA%\Quasar`/profile ACLs; Linux/macOS uses `~/.config/Quasar` and owner-only credentials-file permissions when supported; all platforms mention the `QUASAR_DATA_DIR` override and Data Protection keyring dependency. + - Platform-specific storage text: Windows/Linux/macOS report the Quasar install directory as the default storage root, all platforms mention the `QUASAR_DATA_DIR` override and Data Protection keyring dependency, and Linux/macOS mention owner-only credentials-file permissions when supported. - `MudTextField` with `InputType.Password` and a key adornment icon. - Cancel / Save buttons; Save is disabled while the field is blank. - **`Save`** — trims the key, returns `DialogResult.Ok(key)`. diff --git a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md index 1b58fe2..ae4afb6 100644 --- a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md +++ b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md @@ -28,10 +28,10 @@ Namespace: `Quasar.Services` | `GetWindowsMagnetarLauncherFileName(runtime)` | Maps `NetFramework48` → `MagnetarLegacy.exe`, otherwise `MagnetarInterim.exe`. | | `FindWindowsMagnetarSource(extractRoot)` | Locates the archive's `Magnetar/` folder by the `MagnetarInterim.exe` that has a sibling `Libraries/` directory. | | `ResolveDedicatedServer64PathAsync(...)` | Priority order: path inferred from a DS executable → `DedicatedServer64OverridePath` option → directory adjacent to the launcher → managed steamcmd install (if `PreferManagedDedicatedServerInstall`) → well-known Steam install locations. Throws if none valid. | -| `TryEnsureManagedDedicatedServerInstallAsync(ct, steamCmdPath?, progress?)` | Runs `steamcmd +app_update 298740 validate`; on non-Windows forces Windows platform type; locked by `_dedicatedServerInstallLock`; tries up to three attempts before returning failure; falls back to a prior valid install on final failure and reports Dedicated Server download/install phase with attempt count when a progress sink is provided. | +| `TryEnsureManagedDedicatedServerInstallAsync(ct, steamCmdPath?, progress?)` | Runs `steamcmd +app_update 298740 validate`; on non-Windows forces Windows platform type; locked by `_dedicatedServerInstallLock`; tries up to three attempts before returning failure; falls back to a prior valid install on final failure and reports Dedicated Server download/install phase with attempt count when a progress sink is provided. SteamCMD waits are linked to host shutdown and kill the whole process tree on cancellation. | | `ResolveSteamCmdPathAsync(ct)` | `SteamCmdPath` option → managed install dir → `PATH` → `TryEnsureManagedSteamCmdInstallAsync`. | | `EnsureManagedSteamCmdInstallAsync(progress?, ct)` | Downloads/extracts managed SteamCMD when missing, reports archive percentage when content length is available, sets exec bits, and is locked by `_steamCmdInstallLock`. | -| `RunSteamCmdAsync(...)` | Runs SteamCMD commands such as `+quit` for native-runtime preparation and throws with trimmed stdout/stderr on failure. | +| `RunSteamCmdAsync(...)` | Runs SteamCMD commands such as `+quit` for native-runtime preparation, links waits to host shutdown, kills the process tree on cancellation, and throws with trimmed stdout/stderr on failure. | | `ResolveNativeLibrarySearchPaths()` | Linux-only helper that prefers Quasar's managed SteamCMD `linux64/` runtime folder when it contains `steamclient.so`, `libtier0_s.so`, and `libvstdlib_s.so`; the supervisor prepends this path to `LD_LIBRARY_PATH` so Steam GameServer init can find Steam's native runtime on fresh headless hosts. | | `CopyToFileWithProgressAsync(...)` | Streams an HTTP response body to disk and reports integer percentage when `Content-Length` is known, otherwise reports indeterminate progress. | | `ExtractArchive / DetectArchiveKind` | Dispatches to BCL `ZipArchive` or SharpCompress (`.tar.gz`, `.7z`) by 8-byte magic header + extension. | @@ -55,7 +55,8 @@ Internal enum `ArchiveKind` (`Unknown`, `Zip`, `TarGz`, `SevenZip`). Private Mag - SharpCompress — `ArchiveFactory`, `IArchiveEntry`, `ReaderOptions` - BCL `System.IO.Compression.ZipArchive`, `System.Diagnostics.Process` - `IHttpClientFactory` (5-minute download timeout) +- `IHostApplicationLifetime` (host shutdown token for SteamCMD ownership) ## Notes -Each install operation has its own `SemaphoreSlim(1,1)` so multiple servers starting at once cannot trigger duplicate installs. Magnetar checks always attempt to resolve the current configured archive source unless a successful GitHub release resolution is still inside its five-minute cooldown; if the installed marker already matches the latest GitHub release tag + asset name, the archive is not downloaded again. Dedicated Server checks use SteamCMD `app_update 298740 validate` and report the detected DS version when available. The DS version is read first from `SpaceEngineers.Game.SpaceEngineersGame.SE_VERSION` inside `SpaceEngineers.Game.dll`, formatted the same way as the game build number (for example `1.209.024`), and includes the server build suffix when `SERVER_BUILD_NUMBER` is present. File-version fallbacks ignore placeholder assembly versions such as `1.0.0`. If the latest check fails while a launcher already exists, Quasar logs the failure and continues with the installed runtime instead of blocking a server start. On Linux the Magnetar launcher is resolved to the actual apphost binary under `Bin/` rather than the wrapper script, so Quasar starts it directly (Bin/ as working directory) and the tracked PID is the server's own — essential for cross-restart adoption. The two OS layouts differ: Windows extracts a single `Magnetar/` folder holding both launcher exes plus a `Libraries/` subfolder; Linux stages the Interim build behind a top-level wrapper with the apphost under `Bin/`. On Windows the per-server `ManagedRuntime` selects `MagnetarInterim.exe` (.NET 10) or `MagnetarLegacy.exe` (.NET Framework 4.8); on non-Windows hosts a `NetFramework48` selection is silently downgraded to `DotNet10`. On Linux/macOS, SteamCMD uses `+@sSteamCmdForcePlatformType windows` to fetch the Windows DS binaries, and exec bits are applied via `File.SetUnixFileMode`; Quasar-managed SteamCMD's `linux64/` runtime is prepared during startup readiness and preferred for `NativeLibrarySearchPaths`. `DedicatedServer64` validation requires the launcher plus core assemblies (`SpaceEngineers.Game.dll`, `VRage.dll`, `Sandbox.Game.dll`) so thin or corrupt DS folders are rejected earlier. Archive entries that resolve outside the extraction root are rejected. +Each install operation has its own `SemaphoreSlim(1,1)` so multiple servers starting at once cannot trigger duplicate installs. Magnetar checks always attempt to resolve the current configured archive source unless a successful GitHub release resolution is still inside its five-minute cooldown; if the installed marker already matches the latest GitHub release tag + asset name, the archive is not downloaded again. Dedicated Server checks use SteamCMD `app_update 298740 validate` and report the detected DS version when available; Quasar owns those SteamCMD processes by linking their waits to caller cancellation plus `ApplicationStopping` and killing the whole process tree on cancellation so shell wrappers do not survive shutdown. The post-kill wait is bounded to ten seconds so host shutdown cannot hang on an unreaped SteamCMD child. The DS version is read first from `SpaceEngineers.Game.SpaceEngineersGame.SE_VERSION` inside `SpaceEngineers.Game.dll`, formatted the same way as the game build number (for example `1.209.024`), and includes the server build suffix when `SERVER_BUILD_NUMBER` is present. File-version fallbacks ignore placeholder assembly versions such as `1.0.0`. If the latest check fails while a launcher already exists, Quasar logs the failure and continues with the installed runtime instead of blocking a server start. On Linux the Magnetar launcher is resolved to the actual apphost binary under `Bin/` rather than the wrapper script, so Quasar starts it directly (Bin/ as working directory) and the tracked PID is the server's own — essential for cross-restart adoption. The two OS layouts differ: Windows extracts a single `Magnetar/` folder holding both launcher exes plus a `Libraries/` subfolder; Linux stages the Interim build behind a top-level wrapper with the apphost under `Bin/`. On Windows the per-server `ManagedRuntime` selects `MagnetarInterim.exe` (.NET 10) or `MagnetarLegacy.exe` (.NET Framework 4.8); on non-Windows hosts a `NetFramework48` selection is silently downgraded to `DotNet10`. On Linux/macOS, SteamCMD uses `+@sSteamCmdForcePlatformType windows` to fetch the Windows DS binaries, and exec bits are applied via `File.SetUnixFileMode`; Quasar-managed SteamCMD's `linux64/` runtime is prepared during startup readiness and preferred for `NativeLibrarySearchPaths`. `DedicatedServer64` validation requires the launcher plus core assemblies (`SpaceEngineers.Game.dll`, `VRage.dll`, `Sandbox.Game.dll`) so thin or corrupt DS folders are rejected earlier. Archive entries that resolve outside the extraction root are rejected. diff --git a/Docs/WindowsDeploymentAndUpdates.md b/Docs/WindowsDeploymentAndUpdates.md index a1a2da1..af2b5f5 100644 --- a/Docs/WindowsDeploymentAndUpdates.md +++ b/Docs/WindowsDeploymentAndUpdates.md @@ -13,7 +13,7 @@ out of scope; see `Docs/QuasarArchitecture.md`). Linux packager, under `artifacts/windows/`: - `quasar-installer-windows.zip` - - top-level `quasar-installer-windows\` directory + - top-level `Quasar\` directory - `Quasar.exe` Bootstrap launcher - `install.ps1` - `uninstall.ps1` @@ -30,7 +30,7 @@ carries one combined `SHA256SUMS` (regenerated by the release job over every Lin and Windows archive). Its entries are ` `, matching the format the updater verifies. The web archive is created without a base directory, symmetric with the worker's `ZipFile.ExtractToDirectory`; the -installer archive includes its `quasar-installer-windows\` base directory so a +installer archive includes its `Quasar\` base directory so a manual extract does not spill files into the current folder. Version normalization is identical to `scripts/package-linux-release.sh`: the same @@ -65,21 +65,24 @@ Windows archives in that combined release. ## First Start The Scheduled Task runs Bootstrap as `Quasar.exe serve --quiet --service` from -the install directory (default `%ProgramFiles%\Quasar`). Bootstrap is the direct -task action — no `cmd.exe` wrapper — so Task Scheduler's job object covers the -Bootstrap process itself, and stopping the task terminates it cleanly. +the install directory (the extracted installer root by default). Bootstrap is +the direct task action — no `cmd.exe` wrapper — so Task Scheduler's job object +covers the Bootstrap process itself, and stopping the task terminates it cleanly. If Bootstrap has no usable `Updates/active-release.json` and no packaged `WebService/Quasar.exe`, it downloads the latest Windows web asset from GitHub and extracts it under: ```text -%APPDATA%\Quasar\ManagedRuntime\WebService\ +\ManagedRuntime\WebService\ ``` Then it writes `Updates/active-release.json` pointing at the managed active worker. `Updates\Staged\` is reserved for not-yet-activated update payloads. The downloaded archive must match the release's `SHA256SUMS` entry before extraction. +On startup, Bootstrap also migrates a legacy default data root at +`%APPDATA%\Quasar` into the install root unless `QUASAR_DATA_DIR` points to a +custom directory. ## UI Worker Updates @@ -90,12 +93,12 @@ default and lists selectable `quasar-web-win-x64.zip` releases on automatically after its `SHA256SUMS` entry is verified; with it disabled, releases remain queued until the operator stages the selected version. Activation is explicit; the UI copies the staged payload into -`%APPDATA%\Quasar\ManagedRuntime\WebService\`, clears stale staged +`\ManagedRuntime\WebService\`, clears stale staged payloads, and Bootstrap drains the old worker, starts the managed `Quasar.exe` on the same port, and leaves managed Magnetar servers running. Staging also resolves `appsettings.json`. Quasar uses the stored release base in -the data directory (`%APPDATA%\Quasar\Updates\appsettings.base.json`) as the +the data directory (`\Updates\appsettings.base.json` by default) as the merge base, applies local values from the install directory, and writes the resolved file into the staged worker. If the merge conflicts, auto-staging stops with a warning and `/settings/updates` shows a git-style conflict editor. Resolve @@ -124,7 +127,7 @@ for a restart to verify or refresh the DS install. Bootstrap checks the primary Quasar release stream every 15 minutes. When it finds a genuinely newer `quasar-installer-windows.zip`, it verifies the -`SHA256SUMS` entry, extracts the archive, strips the single top-level installer +`SHA256SUMS` entry, extracts the archive, strips the single top-level `Quasar` directory, and replaces the installed launcher files (renaming a running `.exe` is permitted on Windows). Existing `appsettings.json` is preserved. If the downloaded launcher is byte-identical to the installed launcher, Bootstrap @@ -153,7 +156,7 @@ the Scheduled Task, and exits with install instructions if it is missing. ```powershell # From an extracted quasar-installer-windows.zip, in an elevated PowerShell: -.\install.ps1 # install to %ProgramFiles%\Quasar and register the task +.\install.ps1 # install in place and register the task .\install.ps1 -Start # also start the task immediately ``` @@ -162,7 +165,8 @@ and restarts the launcher on failure. Bootstrap is the direct task executable (`Quasar.exe serve --quiet --service`); service mode is signalled via the `--service` flag rather than environment variables, which also ensures Task Scheduler's job object covers Bootstrap so stopping the task terminates it. It -runs as `SYSTEM` by default; pass `-User ` for a specific service account. +runs as the installing user by default; pass `-InstallDir ` to copy Quasar +elsewhere or `-User ` for a specific service account. ```powershell .\uninstall.ps1 # stop and remove the task diff --git a/Magnetar.Protocol/Runtime/MagnetarPaths.cs b/Magnetar.Protocol/Runtime/MagnetarPaths.cs index d4d3085..b1c0fe1 100644 --- a/Magnetar.Protocol/Runtime/MagnetarPaths.cs +++ b/Magnetar.Protocol/Runtime/MagnetarPaths.cs @@ -6,8 +6,10 @@ namespace Magnetar.Protocol.Runtime; public static class MagnetarPaths { // ------------------------------------------------------------------------- - // Root — everything lives under ~/.config/Quasar (Linux / macOS) or - // %APPDATA%\Quasar (Windows). Override with QUASAR_DATA_DIR. + // Root — everything lives under QUASAR_DATA_DIR when set. Bootstrap sets it + // to the launcher install root for packaged installs after migrating legacy + // ~/.config/Quasar (Linux/macOS) or %APPDATA%\Quasar (Windows) data. + // Without Bootstrap, fall back to the OS application-data directory. // ------------------------------------------------------------------------- public static string GetQuasarDirectory() @@ -87,7 +89,7 @@ public static string GetQuasarBackupsDirectory() => Path.Combine(GetQuasarDirectory(), "Backups"); // ------------------------------------------------------------------------- - // Magnetar server data (~/.config/Quasar/Magnetars//) + // Magnetar server data (/Magnetars//) // ------------------------------------------------------------------------- /// Directory that contains one sub-folder per Magnetar server. @@ -121,7 +123,7 @@ public static string GetQuasarServerAnalyticsPath(string uniqueName) => Path.Combine(GetQuasarServerDirectory(uniqueName), "analytics.jsonl"); // ------------------------------------------------------------------------- - // World templates (~/.config/Quasar/WorldTemplates//) + // World templates (/WorldTemplates//) // ------------------------------------------------------------------------- public static string GetQuasarWorldTemplatesDirectory() => diff --git a/Quasar.Bootstrap/Program.cs b/Quasar.Bootstrap/Program.cs index 2246e8e..f454480 100644 --- a/Quasar.Bootstrap/Program.cs +++ b/Quasar.Bootstrap/Program.cs @@ -28,6 +28,8 @@ internal static class Program public static async Task Main(string[] args) { + BootstrapDataDirectoryMigration.ApplyInstallRootDefault(); + var quiet = args.Any(static arg => string.Equals(arg, "--quiet", StringComparison.OrdinalIgnoreCase)); var openBrowser = args.Any(static arg => string.Equals(arg, "--open-browser", StringComparison.OrdinalIgnoreCase)); var force = args.Any(static arg => string.Equals(arg, "--force", StringComparison.OrdinalIgnoreCase)); @@ -556,6 +558,221 @@ private static bool IsDotNetHost(string processPath) } } +internal static class BootstrapDataDirectoryMigration +{ + private const string DataDirectoryEnvironmentVariable = "QUASAR_DATA_DIR"; + + public static BootstrapDataDirectoryMigrationResult LastResult { get; private set; } = BootstrapDataDirectoryMigrationResult.Empty; + + public static void ApplyInstallRootDefault() + { + LastResult = BootstrapDataDirectoryMigrationResult.Empty; + + var installRoot = NormalizeDirectory(AppContext.BaseDirectory); + if (string.IsNullOrWhiteSpace(installRoot)) + return; + + var legacyRoot = GetLegacyQuasarDirectory(); + var configuredRoot = Environment.GetEnvironmentVariable(DataDirectoryEnvironmentVariable); + if (IsCustomDataDirectory(configuredRoot, installRoot, legacyRoot)) + return; + + var targetRoot = string.IsNullOrWhiteSpace(configuredRoot) || IsSamePath(configuredRoot, legacyRoot) + ? installRoot + : NormalizeDirectory(configuredRoot); + + if (string.IsNullOrWhiteSpace(targetRoot)) + targetRoot = installRoot; + + if (!string.IsNullOrWhiteSpace(legacyRoot) && + !IsSamePath(legacyRoot, targetRoot) && + IsPathUnder(targetRoot, legacyRoot)) + { + Environment.SetEnvironmentVariable(DataDirectoryEnvironmentVariable, legacyRoot); + LastResult = new BootstrapDataDirectoryMigrationResult( + legacyRoot, + targetRoot, + Migrated: false, + ErrorMessage: "Install root is inside the legacy data directory."); + return; + } + + try + { + Directory.CreateDirectory(targetRoot); + var migrated = false; + if (!string.IsNullOrWhiteSpace(legacyRoot) && + !IsSamePath(legacyRoot, targetRoot) && + Directory.Exists(legacyRoot)) + { + MergeDirectoryContents(legacyRoot, targetRoot); + TryRewriteMigratedActiveReleasePointer(legacyRoot, targetRoot); + TryDeleteDirectoryIfEmpty(legacyRoot); + migrated = true; + } + + Environment.SetEnvironmentVariable(DataDirectoryEnvironmentVariable, targetRoot); + LastResult = new BootstrapDataDirectoryMigrationResult(legacyRoot, targetRoot, migrated, string.Empty); + } + catch (Exception exception) + { + if (!string.IsNullOrWhiteSpace(configuredRoot)) + Environment.SetEnvironmentVariable(DataDirectoryEnvironmentVariable, configuredRoot); + + LastResult = new BootstrapDataDirectoryMigrationResult( + legacyRoot, + targetRoot, + Migrated: false, + ErrorMessage: exception.Message); + } + } + + private static bool IsCustomDataDirectory(string? configuredRoot, string installRoot, string legacyRoot) + { + return !string.IsNullOrWhiteSpace(configuredRoot) && + !IsSamePath(configuredRoot, installRoot) && + !IsSamePath(configuredRoot, legacyRoot); + } + + private static string GetLegacyQuasarDirectory() + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + return string.IsNullOrWhiteSpace(appData) + ? string.Empty + : NormalizeDirectory(Path.Combine(appData, "Quasar")); + } + + private static void TryRewriteMigratedActiveReleasePointer(string legacyRoot, string targetRoot) + { + var pointerPath = Path.Combine(targetRoot, "Updates", "active-release.json"); + if (!File.Exists(pointerPath)) + return; + + try + { + var pointer = JsonSerializer.Deserialize( + File.ReadAllText(pointerPath), + LauncherCoordinator.JsonOptions); + if (pointer is null) + return; + + var fileName = RewriteMigratedPath(pointer.FileName, legacyRoot, targetRoot); + var workingDirectory = RewriteMigratedPath(pointer.WorkingDirectory, legacyRoot, targetRoot); + if (string.Equals(fileName, pointer.FileName, StringComparison.Ordinal) && + string.Equals(workingDirectory, pointer.WorkingDirectory, StringComparison.Ordinal)) + { + return; + } + + var rewritten = new QuasarActiveReleasePointer + { + Version = pointer.Version, + FileName = fileName, + Arguments = pointer.Arguments, + WorkingDirectory = workingDirectory, + ActivatedAtUtc = pointer.ActivatedAtUtc, + }; + File.WriteAllText(pointerPath, JsonSerializer.Serialize(rewritten, LauncherCoordinator.JsonOptions)); + } + catch + { + } + } + + private static string RewriteMigratedPath(string value, string legacyRoot, string targetRoot) + { + if (string.IsNullOrWhiteSpace(value) || !Path.IsPathFullyQualified(value)) + return value; + + if (IsSamePath(value, legacyRoot)) + return targetRoot; + + if (!IsPathUnder(value, legacyRoot)) + return value; + + var relativePath = Path.GetRelativePath(NormalizeDirectory(legacyRoot), NormalizeDirectory(value)); + return Path.Combine(targetRoot, relativePath); + } + + private static void MergeDirectoryContents(string sourceDirectory, string destinationDirectory) + { + Directory.CreateDirectory(destinationDirectory); + + foreach (var sourcePath in Directory.EnumerateFiles(sourceDirectory)) + { + var destinationPath = Path.Combine(destinationDirectory, Path.GetFileName(sourcePath)); + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + File.Copy(sourcePath, destinationPath, overwrite: true); + TryDeleteFile(sourcePath); + } + + foreach (var sourceChildDirectory in Directory.EnumerateDirectories(sourceDirectory)) + { + var destinationChildDirectory = Path.Combine(destinationDirectory, Path.GetFileName(sourceChildDirectory)); + MergeDirectoryContents(sourceChildDirectory, destinationChildDirectory); + TryDeleteDirectoryIfEmpty(sourceChildDirectory); + } + } + + private static void TryDeleteFile(string path) + { + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch + { + } + } + + private static void TryDeleteDirectoryIfEmpty(string path) + { + try + { + if (Directory.Exists(path) && !Directory.EnumerateFileSystemEntries(path).Any()) + Directory.Delete(path); + } + catch + { + } + } + + private static string NormalizeDirectory(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + return string.Empty; + + return Path.GetFullPath(path.Trim()).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + private static bool IsSamePath(string? left, string? right) + { + if (string.IsNullOrWhiteSpace(left) || string.IsNullOrWhiteSpace(right)) + return false; + + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return string.Equals(NormalizeDirectory(left), NormalizeDirectory(right), comparison); + } + + private static bool IsPathUnder(string path, string possibleParent) + { + var normalizedPath = NormalizeDirectory(path) + Path.DirectorySeparatorChar; + var normalizedParent = NormalizeDirectory(possibleParent) + Path.DirectorySeparatorChar; + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return normalizedPath.StartsWith(normalizedParent, comparison); + } +} + +internal readonly record struct BootstrapDataDirectoryMigrationResult( + string LegacyPath, + string TargetPath, + bool Migrated, + string ErrorMessage) +{ + public static BootstrapDataDirectoryMigrationResult Empty { get; } = new(string.Empty, string.Empty, false, string.Empty); +} + internal sealed class BootstrapOptions { public const string SupervisorName = "Quasar"; @@ -810,6 +1027,23 @@ public WebServiceDiscoveryManifest GetManifest() public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting Quasar..."); + var migration = BootstrapDataDirectoryMigration.LastResult; + if (migration.Migrated) + { + _logger.LogInformation( + "Migrated legacy Quasar data directory from {LegacyPath} to {TargetPath}.", + migration.LegacyPath, + migration.TargetPath); + } + else if (!string.IsNullOrWhiteSpace(migration.ErrorMessage)) + { + _logger.LogWarning( + "Failed migrating legacy Quasar data directory from {LegacyPath} to {TargetPath}: {Message}", + migration.LegacyPath, + migration.TargetPath, + migration.ErrorMessage); + } + Directory.CreateDirectory(MagnetarPaths.GetWebServiceDirectory()); Directory.CreateDirectory(MagnetarPaths.GetQuasarUpdatesDirectory()); await EnsureInitialWebReleaseAvailableAsync(cancellationToken).ConfigureAwait(false); @@ -1598,6 +1832,7 @@ private async Task ActivateSpecificReleaseAsync(QuasarActiveReleasePointer point startInfo.Environment["QUASAR_LAUNCHER_TOKEN"] = _launcherToken; startInfo.Environment["QUASAR_BOOTSTRAP_VERSION"] = _options.Version; startInfo.Environment["QUASAR_INSTALL_DIR"] = AppContext.BaseDirectory; + startInfo.Environment["QUASAR_DATA_DIR"] = MagnetarPaths.GetQuasarDirectory(); startInfo.Environment["QUASAR_PRESERVE_SERVERS_ON_SHUTDOWN"] = _options.PreserveServersOnShutdown ? "true" : "false"; if (_foregroundOptions.IsForeground) startInfo.Environment["QUASAR_CONSOLE_LOGGING"] = "true"; diff --git a/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor b/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor index ad35297..4a4edb9 100644 --- a/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor +++ b/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor @@ -75,10 +75,10 @@ private static string GetPlatformStorageLocation() { if (OperatingSystem.IsWindows()) - return "%APPDATA%\\Quasar, or QUASAR_DATA_DIR when that override is set"; + return "the Quasar install directory, or QUASAR_DATA_DIR when that override is set"; if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) - return "~/.config/Quasar, or QUASAR_DATA_DIR when that override is set"; + return "the Quasar install directory, or QUASAR_DATA_DIR when that override is set"; return "the Quasar data directory, or QUASAR_DATA_DIR when that override is set"; } diff --git a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs index 7eadd9a..347d234 100644 --- a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs +++ b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs @@ -18,6 +18,7 @@ public sealed class ManagedDedicatedServerRuntimeResolver { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); private static readonly TimeSpan MagnetarReleaseCheckCooldown = TimeSpan.FromMinutes(5); + private static readonly TimeSpan SteamCmdKillWaitTimeout = TimeSpan.FromSeconds(10); private const string MagnetarLauncherName = "MagnetarInterim"; private const string MagnetarReleaseMarkerFileName = ".quasar-magnetar-release.json"; private const string DedicatedServerAppId = "298740"; @@ -65,6 +66,7 @@ public sealed class ManagedDedicatedServerRuntimeResolver private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly ManagedRuntimeOptions _options; + private readonly IHostApplicationLifetime _lifetime; private readonly SemaphoreSlim _magnetarInstallLock = new(1, 1); private readonly SemaphoreSlim _steamCmdInstallLock = new(1, 1); private readonly SemaphoreSlim _dedicatedServerInstallLock = new(1, 1); @@ -75,11 +77,13 @@ public sealed class ManagedDedicatedServerRuntimeResolver public ManagedDedicatedServerRuntimeResolver( ILogger logger, IHttpClientFactory httpClientFactory, - ManagedRuntimeOptions options) + ManagedRuntimeOptions options, + IHostApplicationLifetime lifetime) { _logger = logger; _httpClientFactory = httpClientFactory; _options = options; + _lifetime = lifetime; } public async Task ResolveAsync( @@ -1091,21 +1095,20 @@ private async Task TryEnsureManagedDedicatedServerInstallAsync( return string.Empty; } - var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); - await process.WaitForExitAsync(cancellationToken); - var stdout = await stdoutTask; - var stderr = await stderrTask; + var result = await WaitForSteamCmdProcessAsync( + process, + $"managed DS install attempt {attempt}/{DedicatedServerInstallMaxAttempts}", + cancellationToken); - if (process.ExitCode != 0) + if (result.ExitCode != 0) { _logger.LogWarning( "steamcmd failed installing/updating managed DS on attempt {Attempt}/{MaxAttempts}. ExitCode={ExitCode}. Stdout={Stdout}. Stderr={Stderr}", attempt, DedicatedServerInstallMaxAttempts, - process.ExitCode, - TrimForLog(stdout), - TrimForLog(stderr)); + result.ExitCode, + TrimForLog(result.Stdout), + TrimForLog(result.Stderr)); if (attempt < DedicatedServerInstallMaxAttempts) continue; @@ -1315,16 +1318,76 @@ private async Task RunSteamCmdAsync( throw new InvalidOperationException($"Failed starting steamcmd while {action}.", exception); } - var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); - await process.WaitForExitAsync(cancellationToken); + var result = await WaitForSteamCmdProcessAsync(process, action, cancellationToken); + + if (result.ExitCode != 0) + { + throw new InvalidOperationException( + $"steamcmd failed while {action}. ExitCode={result.ExitCode}. Stdout={TrimForLog(result.Stdout)}. Stderr={TrimForLog(result.Stderr)}"); + } + } + + private async Task WaitForSteamCmdProcessAsync( + Process process, + string action, + CancellationToken cancellationToken) + { + using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + _lifetime.ApplicationStopping); + + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + + try + { + await process.WaitForExitAsync(linkedCancellation.Token); + } + catch (OperationCanceledException) + { + KillSteamCmdProcessTree(process, action); + await WaitForKilledSteamCmdAsync(process, action); + throw; + } + var stdout = await stdoutTask; var stderr = await stderrTask; + return new SteamCmdProcessResult(process.ExitCode, stdout, stderr); + } - if (process.ExitCode != 0) + private void KillSteamCmdProcessTree(Process process, string action) + { + try { - throw new InvalidOperationException( - $"steamcmd failed while {action}. ExitCode={process.ExitCode}. Stdout={TrimForLog(stdout)}. Stderr={TrimForLog(stderr)}"); + if (process.HasExited) + return; + + _logger.LogInformation("Stopping steamcmd after cancellation while {Action}.", action); + process.Kill(entireProcessTree: true); + } + catch (InvalidOperationException) + { + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Failed stopping steamcmd after cancellation while {Action}.", action); + } + } + + private async Task WaitForKilledSteamCmdAsync(Process process, string action) + { + try + { + using var timeout = new CancellationTokenSource(SteamCmdKillWaitTimeout); + await process.WaitForExitAsync(timeout.Token); + } + catch (OperationCanceledException) + { + _logger.LogWarning("Timed out waiting for killed steamcmd while {Action}.", action); + } + catch (Exception exception) + { + _logger.LogDebug(exception, "Failed waiting for killed steamcmd while {Action}.", action); } } @@ -1918,6 +1981,8 @@ private sealed class GitHubAsset [JsonPropertyName("browser_download_url")] public string BrowserDownloadUrl { get; set; } = string.Empty; } + + private sealed record SteamCmdProcessResult(int ExitCode, string Stdout, string Stderr); } internal enum ArchiveKind diff --git a/install.sh b/install.sh index dbd2e02..7307dbe 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SERVICE_NAME="quasar" INSTALL_DIR="" DATA_DIR="" +PACKAGED_INSTALL=false CONFIGURATION="Release" RUNTIME="linux-x64" ENABLE_SERVICE=true @@ -22,8 +23,9 @@ usage() { cat < Install directory (default: /.local/share/Quasar) - --data-dir Quasar data directory (default: /.config/Quasar) + --install-dir Install directory (default: extracted installer root; source installs use /.local/share/Quasar) + --data-dir Quasar data directory (default: install directory) --service-name systemd service name (default: quasar) --user User to run Quasar as (system installs only; default: sudo caller) --user-service Install a user systemd service (default) @@ -417,7 +419,11 @@ while [[ $# -gt 0 ]]; do esac done -if [[ "$SKIP_BUILD" == "false" && -x "$SCRIPT_DIR/Quasar" && ! -f "$SCRIPT_DIR/Quasar.Bootstrap/Quasar.Bootstrap.csproj" ]]; then +if [[ -x "$SCRIPT_DIR/Quasar" && ! -f "$SCRIPT_DIR/Quasar.Bootstrap/Quasar.Bootstrap.csproj" ]]; then + PACKAGED_INSTALL=true +fi + +if [[ "$SKIP_BUILD" == "false" && "$PACKAGED_INSTALL" == "true" ]]; then SKIP_BUILD=true fi @@ -464,10 +470,14 @@ if [[ -z "$RUN_HOME" ]]; then exit 1 fi if [[ -z "$INSTALL_DIR" ]]; then - INSTALL_DIR="$RUN_HOME/.local/share/Quasar" + if [[ "$PACKAGED_INSTALL" == "true" ]]; then + INSTALL_DIR="$SCRIPT_DIR" + else + INSTALL_DIR="$RUN_HOME/.local/share/Quasar" + fi fi if [[ -z "$DATA_DIR" ]]; then - DATA_DIR="$RUN_HOME/.config/Quasar" + DATA_DIR="$INSTALL_DIR" fi INSTALL_DIR="$(realpath -m "$INSTALL_DIR")" DATA_DIR="$(realpath -m "$DATA_DIR")" @@ -486,11 +496,6 @@ case "$DATA_DIR" in ;; esac -if [[ "$DATA_DIR" == "$INSTALL_DIR" || "$DATA_DIR" == "$INSTALL_DIR"/* ]]; then - echo "Data directory must not be inside the install directory: $DATA_DIR" >&2 - exit 1 -fi - require_dotnet_installation write_service_unit() { @@ -578,6 +583,20 @@ restart_service() { fi } +copy_if_different() { + local source="$1" + local destination="$2" + if [[ ! -f "$source" ]]; then + return + fi + + if [[ "$(realpath -m "$source")" == "$(realpath -m "$destination")" ]]; then + return + fi + + cp -a "$source" "$destination" +} + cleanup_old_opt_install() { local old_install_dir="/opt/quasar" if [[ "$INSTALL_MODE" != "user" || "$INSTALL_DIR" == "$old_install_dir" || ! -x "$old_install_dir/uninstall.sh" ]]; then @@ -665,10 +684,18 @@ if [[ "$SKIP_BUILD" == "true" ]]; then echo "Existing publish output not found: $SOURCE_DIR" >&2 exit 1 fi - find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 \ - ! -name "quasar-*.tar.gz" \ - ! -name "quasar-*.zip" \ - -exec cp -a -- {} "$PUBLISH_DIR/" \; + if [[ "$(realpath -m "$SOURCE_DIR")" == "$INSTALL_DIR" ]]; then + for source_entry in Quasar appsettings.json install.sh uninstall.sh quasar-renice.c README.md WebService; do + if [[ -e "$SOURCE_DIR/$source_entry" ]]; then + cp -a -- "$SOURCE_DIR/$source_entry" "$PUBLISH_DIR/" + fi + done + else + find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 \ + ! -name "quasar-*.tar.gz" \ + ! -name "quasar-*.zip" \ + -exec cp -a -- {} "$PUBLISH_DIR/" \; + fi else BUILD_VERSION="$(resolve_build_version)" NUGET_VERSION="$(normalize_nuget_version "$BUILD_VERSION")" @@ -685,18 +712,16 @@ else install -d -m 0755 "$INSTALL_DIR" install -d -m 0755 "$DATA_DIR" fi -find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + -cp -a "$PUBLISH_DIR/." "$INSTALL_DIR/" -if [[ -f "$SCRIPT_DIR/install.sh" ]]; then - cp -a "$SCRIPT_DIR/install.sh" "$INSTALL_DIR/install.sh" -fi -if [[ -f "$SCRIPT_DIR/uninstall.sh" ]]; then - cp -a "$SCRIPT_DIR/uninstall.sh" "$INSTALL_DIR/uninstall.sh" +if [[ "$DATA_DIR" != "$INSTALL_DIR" && "$DATA_DIR" != "$INSTALL_DIR"/* ]]; then + find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + fi +cp -a "$PUBLISH_DIR/." "$INSTALL_DIR/" +copy_if_different "$SCRIPT_DIR/install.sh" "$INSTALL_DIR/install.sh" +copy_if_different "$SCRIPT_DIR/uninstall.sh" "$INSTALL_DIR/uninstall.sh" if [[ -f "$SCRIPT_DIR/tools/quasar-renice.c" ]]; then - cp -a "$SCRIPT_DIR/tools/quasar-renice.c" "$INSTALL_DIR/quasar-renice.c" + copy_if_different "$SCRIPT_DIR/tools/quasar-renice.c" "$INSTALL_DIR/quasar-renice.c" elif [[ -f "$SCRIPT_DIR/quasar-renice.c" ]]; then - cp -a "$SCRIPT_DIR/quasar-renice.c" "$INSTALL_DIR/quasar-renice.c" + copy_if_different "$SCRIPT_DIR/quasar-renice.c" "$INSTALL_DIR/quasar-renice.c" fi if [[ "${EUID}" -eq 0 ]]; then chown -R "$RUN_USER:$RUN_GROUP" "$INSTALL_DIR" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index a950be3..09fab03 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -8,7 +8,7 @@ [CmdletBinding()] param( - [string]$InstallDir = "$env:ProgramFiles\Quasar", + [string]$InstallDir, [string]$TaskName = 'Quasar', [string]$Configuration = 'Release', [string]$Runtime = 'win-x64', @@ -75,13 +75,23 @@ if (-not $principalCheck.IsInRole([System.Security.Principal.WindowsBuiltInRole] $localExe = Join-Path $ScriptDir 'Quasar.exe' $bootstrapProject = Join-Path $RepoDir 'Quasar.Bootstrap\Quasar.Bootstrap.csproj' +$packagedInstall = (Test-Path -LiteralPath $localExe) -and -not (Test-Path -LiteralPath $bootstrapProject) $skipBuild = $NoBuild.IsPresent -if (-not $skipBuild -and (Test-Path -LiteralPath $localExe) -and -not (Test-Path -LiteralPath $bootstrapProject)) { +if (-not $skipBuild -and $packagedInstall) { # Running next to an extracted release zip: install those binaries directly. $skipBuild = $true } +if ([string]::IsNullOrWhiteSpace($InstallDir)) { + if ($packagedInstall) { + $InstallDir = $ScriptDir + } + else { + $InstallDir = "$env:ProgramFiles\Quasar" + } +} + function Normalize-VersionComponent { param([string]$Value) if ([string]::IsNullOrEmpty($Value) -or $Value -notmatch '^[0-9]+$') { return '0' } @@ -195,7 +205,6 @@ try { Write-Host "Installing Quasar to $InstallDir..." New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null - Get-ChildItem -LiteralPath $InstallDir -Force | Remove-Item -Recurse -Force Get-ChildItem -LiteralPath $staging -Force | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $InstallDir $_.Name) -Recurse -Force } } @@ -287,6 +296,7 @@ Installed Quasar. Scheduled task: $TaskName Install dir: $InstallDir +Data dir: $InstallDir Run as: $runAs Web UI: $uiUrl diff --git a/scripts/package-linux-release.sh b/scripts/package-linux-release.sh index be6a62a..8451be2 100755 --- a/scripts/package-linux-release.sh +++ b/scripts/package-linux-release.sh @@ -11,7 +11,7 @@ ASSEMBLY_FILE_VERSION="1.0.0" NUGET_VERSION="$VERSION" WEB_ARCHIVE_NAME="quasar-web-linux-x64.tar.gz" INSTALLER_ARCHIVE_NAME="quasar-installer-linux.tar.gz" -INSTALLER_ROOT_NAME="quasar-installer-linux" +INSTALLER_ROOT_NAME="Quasar" normalize_version_component() { local value="${1:-0}" diff --git a/scripts/package-windows-release.ps1 b/scripts/package-windows-release.ps1 index f8e1164..32f64c1 100644 --- a/scripts/package-windows-release.ps1 +++ b/scripts/package-windows-release.ps1 @@ -24,7 +24,7 @@ $Version = if ($env:VERSION) { $env:VERSION } else { '' } $DefaultAssemblyFileVersion = '1.0.0' $WebArchiveName = 'quasar-web-win-x64.zip' $InstallerArchiveName = 'quasar-installer-windows.zip' -$InstallerRootName = 'quasar-installer-windows' +$InstallerRootName = 'Quasar' function Normalize-VersionComponent { param([string]$Value) diff --git a/scripts/readme-install-linux.md b/scripts/readme-install-linux.md index 01365bd..fa7f702 100644 --- a/scripts/readme-install-linux.md +++ b/scripts/readme-install-linux.md @@ -1,14 +1,14 @@ ## Install and run on Linux (x64) You downloaded **`quasar-installer-linux.tar.gz`**. It contains one -`quasar-installer-linux/` folder with the Quasar launcher (`Quasar`), the +`Quasar/` folder with the Quasar launcher (`Quasar`), the `install.sh` / `uninstall.sh` scripts, and a default `appsettings.json`. ### Run in the foreground ```bash tar -xzf quasar-installer-linux.tar.gz -cd quasar-installer-linux +cd Quasar ./Quasar serve ``` @@ -22,14 +22,15 @@ port is configurable — see [Configuration](Docs/Configuration.md). Install the **.NET 10 runtime** before running `install.sh`. ```bash -tar -xzf quasar-installer-linux.tar.gz -C /tmp -/tmp/quasar-installer-linux/install.sh --start +mkdir -p ~/.local/share/Quasar +tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1 +~/.local/share/Quasar/install.sh --start ``` -This installs Quasar to `~/.local/share/Quasar`, creates the user's -`~/.config/Quasar` data directory, and starts the user `quasar.service`. Pass -`--system` with `sudo` for a machine-wide service, or `--data-dir ` to -store Quasar state elsewhere. The web UI is then served at +This installs Quasar in the extracted folder, keeps Quasar state in the same +folder by default, and starts the user `quasar.service`. Pass `--system` with +`sudo` for a machine-wide service, `--install-dir ` to copy it elsewhere, +or `--data-dir ` to store Quasar state elsewhere. The web UI is then served at `http://localhost:8080`. In the installed user service, the UI **Shutdown Quasar** action requests `systemctl --user stop quasar.service`. Manage the service with: @@ -44,7 +45,7 @@ systemctl --user restart quasar.service ```bash ~/.local/share/Quasar/uninstall.sh # stop and remove the user service -~/.local/share/Quasar/uninstall.sh --purge # also delete ~/.local/share/Quasar +~/.local/share/Quasar/uninstall.sh --purge # also delete the install folder ``` The uninstall script stops `quasar.service` before removing it. diff --git a/scripts/readme-install-windows.md b/scripts/readme-install-windows.md index d318ca7..0816f4f 100644 --- a/scripts/readme-install-windows.md +++ b/scripts/readme-install-windows.md @@ -1,13 +1,13 @@ ## Install and run on Windows (x64) You downloaded **`quasar-installer-windows.zip`**. It contains one -`quasar-installer-windows\` folder with the Quasar launcher (`Quasar.exe`), the +`Quasar\` folder with the Quasar launcher (`Quasar.exe`), the `install.ps1` / `uninstall.ps1` scripts, and a default `appsettings.json`. The steps below assume you have extracted the zip, for example: ```powershell Expand-Archive quasar-installer-windows.zip -DestinationPath C:\quasar -cd C:\quasar\quasar-installer-windows +cd C:\quasar\Quasar ``` ### Run in the foreground @@ -31,11 +31,11 @@ Run from an **elevated PowerShell** (Administrator): .\install.ps1 -Start ``` -This installs Quasar to `%ProgramFiles%\Quasar` and registers a **Scheduled -Task** named `Quasar` that starts the launcher at boot and restarts it on -failure. The web UI is then served at . Pass -`-User ` to run as a specific service account instead of the current -user. +This installs Quasar in the extracted folder, keeps Quasar state in the same +folder by default, and registers a **Scheduled Task** named `Quasar` that starts +the launcher at boot and restarts it on failure. The web UI is then served at +. Pass `-InstallDir ` to copy it elsewhere, or +`-User ` to run as a specific service account instead of the current user. Manage the task: @@ -48,7 +48,6 @@ Stop-ScheduledTask -TaskName Quasar ### Uninstall ```powershell -cd "$env:ProgramFiles\Quasar" .\uninstall.ps1 # stop and remove the Scheduled Task .\uninstall.ps1 -Purge # also delete the install directory ``` diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index ed82aa0..f240ce5 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -2,13 +2,13 @@ # Windows analogue of uninstall.sh. # # Stops and removes the Quasar Scheduled Task registered by install.ps1. Runtime -# and config data under %APPDATA%\Quasar is left untouched. Pass -Purge to also -# delete the install directory. +# and config data under the install directory is left untouched. Pass -Purge to +# also delete the install directory. [CmdletBinding()] param( [string]$TaskName = 'Quasar', - [string]$InstallDir = "$env:ProgramFiles\Quasar", + [string]$InstallDir = $PSScriptRoot, [switch]$Purge ) diff --git a/uninstall.sh b/uninstall.sh index 3dc3114..4362082 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -12,11 +12,12 @@ usage() { Usage: ./uninstall.sh [options] Stops and removes the Quasar systemd service installed by install.sh. -Runtime/config data under the Quasar user's home directory is not removed. +Runtime/config data under the install directory is not removed unless --purge is +passed. Options: --service-name systemd service name (default: quasar) - --install-dir Install directory (default: ~/.local/share/Quasar) + --install-dir Install directory (default: script directory) --user-service Remove a user systemd service (default) --system Remove a system service under /etc/systemd/system --purge Also remove the install directory diff --git a/whitelabel-theme-configurator.md b/whitelabel-theme-configurator.md index 4071595..bdd96b2 100644 --- a/whitelabel-theme-configurator.md +++ b/whitelabel-theme-configurator.md @@ -9,7 +9,7 @@ Quasar currently has a static theme (`QuasarTheme.cs`) with hardcoded light/dark ``` BrandingService (singleton) - └── loads/saves ~/.config/Quasar/branding.json + └── loads/saves /branding.json └── exposes BuildMudTheme() → MudTheme └── fires Changed event → MainLayout calls StateHasChanged