From 5cbb3f658289d06d24ad5f836790f4320821facf Mon Sep 17 00:00:00 2001 From: Owen de Bree Date: Mon, 22 Jun 2026 19:47:15 +0200 Subject: [PATCH] fix: improve dashboard and lifecycle - Persist dashboard card/list mode and add card-view Create Server, config-profile, and direct-connect port actions. - Copy direct-connect host:port from dashboard cards and the server list, using configured server IP with browser-host fallback for wildcard binds. - Suppress simulation-baseline collection as a warning while keeping the state visible on server cards. - Make Quasar.Agent own !stop, !restart, and !quit. !restart sends AdminRestart so Quasar keeps goal On and relaunches; !quit exits without saving while setting goal Off. - Log Quasar and Magnetar/Quasar.Agent versions on startup. - Show installed Magnetar and Dedicated Server versions/paths on Updates, with separate manual Magnetar and DS check buttons. - Refresh architecture, state-machine, deployment, and generated reference docs. Verified: - dotnet build Quasar.sln -v minimal - git diff --check - Docs/Reference/data/verify_links.py --- Docs/LinuxDeploymentAndUpdates.md | 15 ++ Docs/QuasarArchitecture.md | 16 +- Docs/Reference/Index.md | 29 +-- Docs/Reference/Modules/Magnetar.Protocol.md | 3 +- Docs/Reference/Modules/Quasar.Agent.md | 6 +- Docs/Reference/Modules/Quasar.Components.md | 10 +- Docs/Reference/Modules/Quasar.Host.md | 2 +- .../Reference/Modules/Quasar.Services.Core.md | 8 +- Docs/Reference/TOC.md | 4 +- Docs/Reference/data/manifest.json | 66 +++--- Docs/Reference/data/module_index.json | 33 +-- Docs/Reference/data/reference_graph.json | 8 + .../Transport/WireMessageKind.cs.md | 3 +- .../files/Quasar.Agent/AdminPlugin.cs.md | 16 +- .../files/Quasar.Agent/AgentConnection.cs.md | 6 +- .../files/Quasar.Agent/StopCommand.cs.md | 18 +- .../Components/Dashboard/ServerCard.razor.md | 8 +- .../Quasar/Components/Pages/Home.razor.md | 14 +- .../Quasar/Components/Pages/Servers.razor.md | 9 +- .../Pages/ServersPageDialog.razor.md | 5 +- .../Quasar/Components/Pages/Updates.razor.md | 14 +- Docs/Reference/files/Quasar/Program.cs.md | 4 +- .../Quasar/Services/AgentSocketHandler.cs.md | 8 +- .../Services/DedicatedServerSupervisor.cs.md | 7 +- ...anagedDedicatedServerRuntimeResolver.cs.md | 10 +- .../ManagedRuntimeWarmupService.cs.md | 13 +- .../StateMachines/DedicatedServerLifecycle.md | 27 ++- Docs/WindowsDeploymentAndUpdates.md | 15 ++ .../Transport/WireMessageKind.cs | 1 + Quasar.Agent/AdminPlugin.cs | 128 +++++++++++- Quasar.Agent/AgentConnection.cs | 19 +- Quasar.Agent/StopCommand.cs | 56 +++++ Quasar/Components/Dashboard/ServerCard.razor | 102 ++++++++++ Quasar/Components/Pages/Home.razor | 105 +++++++++- Quasar/Components/Pages/Servers.razor | 73 ++++++- .../Components/Pages/ServersPageDialog.razor | 6 +- Quasar/Components/Pages/Updates.razor | 119 ++++++++++- Quasar/Program.cs | 7 + Quasar/Services/AgentSocketHandler.cs | 20 ++ Quasar/Services/DedicatedServerSupervisor.cs | 58 +++++- .../ManagedDedicatedServerRuntimeResolver.cs | 192 +++++++++++++++++- .../Services/ManagedRuntimeWarmupService.cs | 84 +++++++- 42 files changed, 1184 insertions(+), 163 deletions(-) diff --git a/Docs/LinuxDeploymentAndUpdates.md b/Docs/LinuxDeploymentAndUpdates.md index b8001ec..0756aea 100644 --- a/Docs/LinuxDeploymentAndUpdates.md +++ b/Docs/LinuxDeploymentAndUpdates.md @@ -127,6 +127,21 @@ they differ, Quasar warns that a manual server restart is required. It does not auto-schedule that restart; the operator-triggered stop/start path runs launch preparation and injects the bundled deployable DLL before relaunch. +## Managed Runtime Update Checks + +The Updates page always shows the currently installed Quasar, Bootstrap, +Magnetar, and Space Engineers Dedicated Server versions when Quasar can resolve +them from release metadata or executable file versions. It also shows the +managed runtime install paths and the most recent managed-runtime check time. + +Quasar UI worker and Bootstrap checks use the Quasar release checker interval +(15 minutes by default) and the page's **Check Quasar** button. Managed Magnetar +checks run during startup readiness and then every hour while Quasar is running; +the page's **Check Magnetar** button runs the same check immediately. Managed DS +checks run during startup readiness; **Check Dedicated Server** runs SteamCMD +`app_update 298740 validate` immediately so an operator does not need to wait +for a restart to verify or refresh the DS install. + ## Bootstrap Updates Bootstrap checks the primary Quasar release stream every 15 minutes by default. diff --git a/Docs/QuasarArchitecture.md b/Docs/QuasarArchitecture.md index 3227c68..22642fb 100644 --- a/Docs/QuasarArchitecture.md +++ b/Docs/QuasarArchitecture.md @@ -328,7 +328,8 @@ Quasar should behave like infrastructure/configuration management: - if goal state is `On` and the server crashes, Quasar restarts it according to policy - if goal state is `On` and the server is unhealthy, Quasar evaluates the health policy and recovers it automatically where configured - if goal state is `Off` and the server is running, Quasar stops it -- if an admin stops the server from in-game (Magnetar `!quit` or Quasar Agent `!stop`), the agent reports the shutdown intent and Quasar sets goal state to `Off`, so the server stays stopped instead of being treated as a crash and restarted +- if an admin stops the server from in-game with Quasar Agent `!stop` or `!quit`, the agent reports `AdminStop` and Quasar sets goal state to `Off`, so the server stays stopped instead of being treated as a crash and restarted; `!stop` saves first, while `!quit` exits immediately without saving +- if an admin restarts the server from in-game with Quasar Agent `!restart`, the agent reports `AdminRestart`, Quasar keeps goal state `On`, records process state `Restarting`, and relaunches the server after the save-and-quit exit instead of waiting for a reconnect that will never arrive - operator actions should usually mutate goal state first, then let reconciliation perform the transition This should be treated more like Terraform or other IaC reconciliation than like a passive dashboard. @@ -540,6 +541,15 @@ 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. +The Updates page also shows installed managed-runtime versions independently of +Quasar self-update state: Quasar UI/Bootstrap, Magnetar, and the Space Engineers +Dedicated Server can all be inspected there. Quasar release checks run on the +configured update interval (15 minutes by default) and can be triggered by 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`. + ### Future proxy update flow 1. download or place a new Quasar release into a staged version directory @@ -859,6 +869,10 @@ As of this document: - per-server managed .NET runtime selection now exists on Windows, where Quasar installs both Magnetar builds side-by-side (`MagnetarInterim.exe` on .NET 10, the default, and `MagnetarLegacy.exe` on .NET Framework 4.8) and the runtime resolver launches the build chosen by `DedicatedServerDefinition.ManagedRuntime`; non-Windows hosts always run the .NET 10 build - runtime config preparation now derives a unique `SteamPort` (`ServerPort + 1000`) and `RemoteApiPort` (`ServerPort + 2000`) per server so multiple servers co-hosted on one machine never collide on the SE defaults (8766 / 8080) - server naming across the UI now consistently prefers the operator-configured `DedicatedServerDefinition.DisplayName` over the agent's in-game `ConfigDedicated.ServerName` (the analytics filters/legends, Discord per-server panels, the entities/plugins server selectors, the players list, and the plugin log panel all resolve names this way, falling back to the live agent name and then the unique name) +- Dashboard server view selection now persists in browser local storage while still accepting `?view=list` / `?view=cards` overrides; card and list layouts use the same catalog order by unique name. Dashboard cards expose the assigned config profile as an actionable chip, the server port as a direct-connect copy chip (`host:port`), and the same create-server entry point the list layout already had. +- Quasar Agent now owns in-game `!stop`, `!restart`, and `!quit` semantics for managed servers: `!stop` saves and turns the Quasar goal Off, `!restart` saves and lets Quasar track/relaunch the Restarting state, and `!quit` exits without saving while turning the goal Off. +- startup version logging now records Quasar worker version in the Quasar log and Magnetar/Quasar.Agent version details in the Magnetar-side log path. +- the Updates page now always surfaces installed managed Magnetar and Space Engineers Dedicated Server versions/paths beside Quasar version data, and exposes separate manual update checks for Magnetar and DS. Magnetar checks continue hourly after startup; DS checks run at startup and on explicit request. - 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. diff --git a/Docs/Reference/Index.md b/Docs/Reference/Index.md index c06f7a6..dfae11d 100644 --- a/Docs/Reference/Index.md +++ b/Docs/Reference/Index.md @@ -1,6 +1,6 @@ # Quasar Handbook — File Index -Every documented source file (209 total), alphabetical by path. See the [TOC](TOC.md) for the module-oriented view. +Every documented source file (210 total), alphabetical by path. See the [TOC](TOC.md) for the module-oriented view. | File | Module | Kind | Summary | | --- | --- | --- | --- | @@ -9,6 +9,7 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Magnetar.Protocol/Magnetar.Protocol.csproj](files/Magnetar.Protocol/Magnetar.Protocol.csproj.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | project file | MSBuild project file for the `Magnetar.Protocol` shared contract library. Targets `netstandard2.0` so the assembly can be loaded by both the Quasar Blazor Server supervisor (net8+) and the in-DS `Quasar.Agent` plugin (which runs inside the Space Engineers process). | | [Magnetar.Protocol/Model/AgentHello.cs](files/Magnetar.Protocol/Model/AgentHello.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Handshake payload sent by `Quasar.Agent` to the Quasar supervisor immediately after the WebSocket connection is established. Carries all static identity information needed by the supervisor to register the agent connection. | | [Magnetar.Protocol/Model/AgentSnapshot.cs](files/Magnetar.Protocol/Model/AgentSnapshot.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Periodic snapshot pushed by `Quasar.Agent` to the Quasar supervisor containing the full observable state of one running SE dedicated server: identity fields, runtime status, scalar performance metrics, current profiler mode, optional profiler timing data, online human players, hidden NPC/bot player ids, kicked players (serving a kick cooldown), recent chat, registered PluginSdk chat commands, recent deaths, and loaded plugin list. | +| [Magnetar.Protocol/Model/ChatCommandSnapshot.cs](files/Magnetar.Protocol/Model/ChatCommandSnapshot.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | DTO describing one registered PluginSdk chat command reported by `Quasar.Agent` in `AgentSnapshot.ChatCommands`. Carries the full command text (`!prefix path`), generated syntax including arguments, help/description text, owner id, root title, minimum promote level, and path segments so Quasar can offer command autocomplete without referencing `PluginSdk`. | | [Magnetar.Protocol/Model/ChatMessageSnapshot.cs](files/Magnetar.Protocol/Model/ChatMessageSnapshot.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Immutable-style DTO representing a single in-game chat message captured for transmission in `AgentSnapshot.RecentChat`, including whether the message was emitted by the dedicated server/Good.bot rather than a player. | | [Magnetar.Protocol/Model/DeathEventSnapshot.cs](files/Magnetar.Protocol/Model/DeathEventSnapshot.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Sealed DTO representing a single player death event captured for transmission in `AgentSnapshot.RecentDeaths`. Carries victim, optional killer and weapon, death classification, and timestamp. | | [Magnetar.Protocol/Model/EntityDeleteRequest.cs](files/Magnetar.Protocol/Model/EntityDeleteRequest.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Minimal request DTO carrying the target entity ID for the `ServerCommandType.DeleteEntity` command. Serialized as JSON into `ServerCommandEnvelope.Payload`. | @@ -33,8 +34,8 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Magnetar.Protocol/Transport/ServerCommandResult.cs](files/Magnetar.Protocol/Transport/ServerCommandResult.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class | Command response DTO sent from the agent back to the Quasar supervisor (via `AgentWireMessage.CommandResult`). Correlates to the originating `ServerCommandEnvelope` via `CommandId`, reports success/failure, and carries an optional structured JSON payload for commands that return data. | | [Magnetar.Protocol/Transport/ServerCommandType.cs](files/Magnetar.Protocol/Transport/ServerCommandType.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | enum | Discriminator enum embedded in `ServerCommandEnvelope.CommandType` that identifies which server-side action the Quasar supervisor is requesting from an agent. | | [Magnetar.Protocol/Transport/WireMessageKind.cs](files/Magnetar.Protocol/Transport/WireMessageKind.cs.md) | [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | class (static) | String constants for the `AgentWireMessage.Kind` discriminator — the shared vocabulary of message types on the Quasar ↔ agent WebSocket channel. Both ends compare against these constants to route each envelope to the correct handler. Values travel on the wire, so they must stay stable across versions. | -| [Quasar.Agent/AdminPlugin.cs](files/Quasar.Agent/AdminPlugin.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it reads `AgentOptions`, configures and applies profiler Harmony patches, registers Quasar's admin chat commands (including the root `!stop` override), builds the `GameBridge`, starts a `PluginLogOutbox` (begun before the connection so startup log lines are buffered), wires `StopCommand` to report an admin stop before `!stop` quits the server, and starts an `AgentConnection`. It drives the game-thread snapshot/profiler refresh on each `Update`, refreshes per-character death subscriptions so respawned players are re-hooked, and handles server termination by sending an `AdminStop` signal to Quasar when shutdown was admin-initiated. | -| [Quasar.Agent/AgentConnection.cs](files/Quasar.Agent/AgentConnection.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends a `Hello` handshake plus periodic `Snapshot` messages, streams buffered plugin log batches from a `PluginLogOutbox`, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. | +| [Quasar.Agent/AdminPlugin.cs](files/Quasar.Agent/AdminPlugin.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it logs Magnetar/Quasar.Agent versions, reads `AgentOptions`, applies profiler Harmony patches, registers Quasar's root admin chat commands (`!stop`, `!restart`, `!quit`), builds the `GameBridge`, starts `PluginLogOutbox`, wires command hooks to early `AdminStop` / `AdminRestart` signals, and starts `AgentConnection`. It drives snapshot/profiler refresh on each `Update`, refreshes death subscriptions, and reports fallback admin shutdown intent when the server terminates outside a Quasar-requested stop. | +| [Quasar.Agent/AgentConnection.cs](files/Quasar.Agent/AgentConnection.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends `Hello` plus periodic `Snapshot` messages, streams buffered plugin log batches, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, sends best-effort admin stop/restart signals before command-triggered exits, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. | | [Quasar.Agent/AgentOptions.cs](files/Quasar.Agent/AgentOptions.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `AgentOptions` is a sealed configuration DTO holding connection-resilience parameters and profiler mode for the agent's WebSocket link to Quasar. Values are read from environment variables set by Quasar when it launches a managed server (or by an operator for standalone servers), with sensible defaults applied when variables are absent or invalid. | | [Quasar.Agent/AgentProfiler.cs](files/Quasar.Agent/AgentProfiler.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | Continuous in-process profiler accumulator used by Harmony method patches and IL call-site transpilers. It records elapsed ticks into numeric call-site accumulators, splits main-thread vs off-thread time, and publishes the latest one-second `ProfilerSnapshot` window for inclusion in the next agent snapshot. | | [Quasar.Agent/AgentProfilerMode.cs](files/Quasar.Agent/AgentProfilerMode.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | enum | Public enum describing the in-DS profiler patch depth selected through `AgentOptions.ProfilerMode`. | @@ -44,13 +45,13 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar.Agent/GameBridge.cs](files/Quasar.Agent/GameBridge.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | `GameBridge` is the central game-thread façade for `AgentConnection`. It collects session telemetry (metrics, current profiler mode/snapshot, human players, hidden NPC/bot player ids, kicked players, chat, registered PluginSdk chat commands, deaths, plugins), builds `AgentHello` / `AgentSnapshot` wire messages, and executes server commands (chat, blocking save, save-and-stop, profiler mode change, kick, ban, promote, clear-kick-cooldown, entity list/delete) by marshalling work onto the game thread via `MySandboxGame.Invoke`. Save/stop commands route through Magnetar PluginSdk `ServerControl` so Quasar observes completed disk saves before treating the command as successful. Metrics include process CPU derived from `Process.TotalProcessorTime`, simspeed/sim CPU from `Sync`, memory, human player count, PCU, active entities/grids, total blocks, floating objects, latest world-save time, and unsaved game-time progress since the last checkpoint. It enumerates loaded plugins from `MyPlugins.Plugins` (including Pulsar child plugins) for runtime inventory, dedupes configured fallback plugin paths against loaded plugins by path stem, parent dev-folder name, manifest ``, and manifest ``, and exposes plugin configuration through `IQuasarConfigProvider` or Magnetar PluginSdk `PluginConfig` reflection. Chat history normalizes dedicated-server/Good.bot messages to author `Server` and marks `ChatMessageSnapshot.IsServerMessage`. | | [Quasar.Agent/PluginLogOutbox.cs](files/Quasar.Agent/PluginLogOutbox.cs.md) | [Quasar.Agent](Modules/Quasar.Agent.md) | class | Bounded, thread-safe buffer that captures plugin log lines emitted in-process by the PluginSdk Quasar log sink and hands them to `AgentConnection` in batches for streaming to Quasar (as `PluginLogBatch` / `WireMessageKind.PluginLogs`). The buffer survives Quasar outages: lines accumulate while disconnected and are flushed on reconnect, so the supervisor's "Recent plugin logs" panel is backfilled rather than losing everything captured while Quasar was down. | | [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 | `StopCommand` is a Quasar-owned PluginSdk command module for the root `!stop` in-game admin command. It overrides Magnetar's earlier `stop` root by being registered later from `AdminPlugin`, acknowledges the caller, reports an admin stop to Quasar through a static hook wired by `AdminPlugin`, then calls `ServerControl.SaveAndQuit()` on a worker task so the world is saved and the dedicated server process exits. | +| [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/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. | -| [Quasar/Components/Dashboard/ServerCard.razor](files/Quasar/Components/Dashboard/ServerCard.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. | +| [Quasar/Components/Dashboard/ServerCard.razor](files/Quasar/Components/Dashboard/ServerCard.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, port/direct-connect chip, config-profile chip, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. | | [Quasar/Components/Dashboard/ServerDetailPanel.razor](files/Quasar/Components/Dashboard/ServerDetailPanel.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Detail panel embedded inside `ServerCard`. When the agent snapshot is absent it shows a waiting/error message and basic process state chips. When a snapshot is present it renders live metrics chips, Refresh/Save buttons (Save disabled while the runtime is `Starting`/`Stopping`/`Restarting`), a chat broadcast field, a players table with player identity/status columns and a rightmost unlabeled action menu column, a recent-chat list, and recent command results. The Plugins chip compares loaded runtime plugins against the assigned config profile's selected plugins, displays `loaded/total`, and turns warning-colored when the loaded count differs from the configured total; if no profile is available it falls back to the aggregate agent plugin metric. The Save chip shows save-in-progress state, or the latest world-save local time plus unsaved in-game progress as `MM:SS`, with a tooltip containing the full local timestamp. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. In both the snapshot-present and snapshot-absent states, an outlined "Affinity " chip (Memory icon) is shown in the metrics chip rows when `Server.CpuAffinity` is set, and mod-download failures captured from runtime output are shown as an explicit error alert. | | [Quasar/Components/Layout/BrandingHeadContent.razor](files/Quasar/Components/Layout/BrandingHeadContent.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | A lightweight head-content component that reactively updates the page favicon whenever `BrandingService` fires its `Changed` event. Rendered inside `MainLayout` so the favicon tracks live branding changes without a full page reload. | | [Quasar/Components/Layout/MainLayout.razor](files/Quasar/Components/Layout/MainLayout.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Top-level application shell layout. Provides the MudBlazor theme/provider setup, theme-configured hover-list CSS variables, a responsive app bar with branding, update notification bell, theme-mode switcher, auth (login/logout) controls, and a Quasar power dialog trigger. It hosts a collapsible side drawer with `NavMenu`, the main content area that renders `@Body`, and a full-screen overlay while Quasar is restarting or shutting down. | @@ -79,7 +80,7 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar/Components/Pages/Error.razor](files/Quasar/Components/Pages/Error.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Standard ASP.NET Core error page rendered at `/Error`. Displays a generic error message and, when a request or activity ID is available, shows it for diagnostics. Advises enabling the Development environment for detailed exceptions and warns against doing so in production. | | [Quasar/Components/Pages/FolderPickerDialog.razor](files/Quasar/Components/Pages/FolderPickerDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | General-purpose server-side folder browser dialog. Renders a navigable directory tree with breadcrumb navigation, bookmark shortcut chips, caller-supplied shortcut chips, hidden-folder toggle, quick filtering for the current folder list, and an optional world-folder validation mode. Returns the selected absolute path on confirmation. Used from `Configs.razor` (dev-folder picker), server instance editors (world-folder picker), and world-template import flows. | | [Quasar/Components/Pages/FolderPickerDialog.razor.css](files/Quasar/Components/Pages/FolderPickerDialog.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Minimal scoped stylesheet for `FolderPickerDialog.razor`. Contains a single rule that removes the default uppercase text transform from the breadcrumb navigation buttons so directory names render in their natural casing. | -| [Quasar/Components/Pages/Home.razor](files/Quasar/Components/Pages/Home.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable dashboard and primary server control surface at `/`. Shows a top-of-dashboard data handling consent prompt until YES/NO is stored, a five-step first-run setup wizard that auto-opens only while no dedicated server has been created yet, a Managed Runtime panel with SteamCMD, Magnetar, and Dedicated Server readiness/download/update progress, summary KPI cards (online servers, players online, health warnings, errors), an optional problem banner, and a switchable server section. The default card view renders `ServerCard`s for live operations; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated, so the landing page does not build hidden list/table DOM. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. The wizard opens full-screen page dialogs for config-template, world-template, and server creation. | +| [Quasar/Components/Pages/Home.razor](files/Quasar/Components/Pages/Home.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable dashboard and primary server control surface at `/`. Shows data-handling consent, the first-run setup wizard, managed-runtime readiness/update progress, KPI cards, a problem banner, and a switchable server section whose Cards/List selection persists in browser local storage. The default card view renders `ServerCard`s in the same unique-name catalog order as the list, includes a Create Server button that opens the server editor immediately through `ServersPageDialog`, and passes config-profile click callbacks to cards; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. | | [Quasar/Components/Pages/Home.razor.css](files/Quasar/Components/Pages/Home.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Scoped stylesheet for `Home.razor`. Provides layout primitives for the setup wizard step container, individual step cards, text/action column sizing, instance-status list rows within the wizard, and the Managed Runtime readiness/progress panel. | | [Quasar/Components/Pages/Hosts.razor](files/Quasar/Components/Pages/Hosts.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Source-only host summary component. It is intentionally not routable for now, so direct navigation to `/hosts` falls through to the app Not Found page. When enabled again, it shows a summary table of every host (physical or virtual machine) that has connected at least one Quasar.Agent. Rows are aggregated from `AgentRegistry` by `HostKey`, displaying the host display name, how many distinct server slots are running on it, how many of those agents are currently connected, and total players online across that host. | | [Quasar/Components/Pages/MergeWorldTemplateModsDialog.razor](files/Quasar/Components/Pages/MergeWorldTemplateModsDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | MudBlazor dialog used from the Configs page to merge mods from a selected world template into the current config profile. It computes a diff of new vs. already-present Workshop IDs and returns only the net-new `QuasarModSelection` list via `DialogResult.Ok`. | @@ -93,10 +94,10 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar/Components/Pages/ServerDeleteDialog.razor](files/Quasar/Components/Pages/ServerDeleteDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | MudBlazor dialog that confirms deletion of a server definition or reports that the definition was removed while leaving the server folder on disk. It shows the affected slug and folder path, lets the user copy the folder path, and closes with `DialogResult.Ok(true)` when the destructive or acknowledgement action is accepted. | | [Quasar/Components/Pages/ServerEditorDialog.razor](files/Quasar/Components/Pages/ServerEditorDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | MudBlazor dialog (no `@page` route) for creating, cloning, or editing a `DedicatedServerDefinition`. It validates server identity/runtime settings, exposes per-server Space Engineers multiplayer-list server/world name overrides, opens inline config/world-template creation dialogs, consumes world-import results that include a config-profile id, can merge missing world-template mods directly into the selected config profile before save, and offers a confirmed reset-world action while editing stopped servers. | | [Quasar/Components/Pages/ServerEditorDialog.razor.css](files/Quasar/Components/Pages/ServerEditorDialog.razor.css.md) | [Quasar.Components](Modules/Quasar.Components.md) | CSS | Scoped stylesheet for `ServerEditorDialog.razor`. Constrains the dialog's scrollable content area and styles the template-select input slots with subtle background tints and focus highlights. Previously named `ServerEditorDialog.razor.css`. | -| [Quasar/Components/Pages/Servers.razor](files/Quasar/Components/Pages/Servers.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel` with the server definition, runtime snapshot, and live agent data. A separate panel lists "unmanaged" agents that report in without a matching server definition. | -| [Quasar/Components/Pages/ServersPageDialog.razor](files/Quasar/Components/Pages/ServersPageDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. | +| [Quasar/Components/Pages/Servers.razor](files/Quasar/Components/Pages/Servers.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status, clickable config-profile names, copyable direct-connect ports, and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel`; a separate panel lists unmanaged agents. | +| [Quasar/Components/Pages/ServersPageDialog.razor](files/Quasar/Components/Pages/ServersPageDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. It can optionally ask the embedded Servers component to open the Create Server editor immediately. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. | | [Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor](files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Small MudBlazor dialog for entering or updating the Steam Web API key used server-side for Workshop search. The key is treated as a password field and returned to the caller as a trimmed string via `DialogResult.Ok(key)`. The dialog highlights the Steam developer API-key page and explains the platform-specific storage protection Quasar applies after saving. | -| [Quasar/Components/Pages/Updates.razor](files/Quasar/Components/Pages/Updates.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | component | Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases. It shows current update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual check/stage/activate actions for the selected UI worker version, can force a detected Bootstrap launcher update to activate immediately when running under Bootstrap, displays configured GitHub release source and asset names, provides controls for including prerelease versions plus choosing automatic or manual UI staging, and renders a git-style `appsettings.json` conflict editor with a copyable conflict-file path when staging cannot auto-merge settings. | +| [Quasar/Components/Pages/Updates.razor](files/Quasar/Components/Pages/Updates.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | component | Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases plus inspecting/updating managed runtime components. It shows current Quasar update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual Quasar check/stage/activate actions, can force a detected Bootstrap launcher update, displays installed Magnetar and Space Engineers Dedicated Server versions/paths at all times, exposes separate manual Magnetar and DS checks, provides prerelease/auto-staging controls, and renders a git-style `appsettings.json` conflict editor when staging cannot auto-merge settings. | | [Quasar/Components/Pages/WorldTemplateFromServerDialog.razor](files/Quasar/Components/Pages/WorldTemplateFromServerDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Lightweight MudBlazor confirmation dialog opened by the Servers page when the user requests to create a world template from a stopped server's current world directory. Collects the template name and description, displays a copyable source world path (read-only), and returns a `TemplateRequest` record to the caller. | | [Quasar/Components/Pages/WorldTemplateQuickImportDialog.razor](files/Quasar/Components/Pages/WorldTemplateQuickImportDialog.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | MudBlazor dialog for adding a Space Engineers world template without leaving the server editor. The first step is a two-tab card: Predefined Worlds lists installed Dedicated Server world/scenario templates for one-click import, while Custom Import validates a name and absolute source path and opens a `FolderPickerDialog` for path browsing. Folder browsing shows shortcut chips for managed Dedicated Server content folders (`Content/CustomWorlds`, `Content/QuickStarts`, `Content/Scenarios`) and remembers the last selected source folder. After a source is chosen, the dialog reads source-world mods from `Sandbox_config.sbc`, lets the user create a config profile, merge into an existing profile, or ignore those mods, and returns the imported template plus an optional config-profile id. | | [Quasar/Components/Pages/WorldTemplates.razor](files/Quasar/Components/Pages/WorldTemplates.razor.md) | [Quasar.Components](Modules/Quasar.Components.md) | Blazor component | Routable page at `/world-templates` for managing reusable Space Engineers world templates. Provides a two-tab import card: a Predefined Worlds tab for one-click importing installed Dedicated Server world/scenario templates without entering a name or path, and a Custom World Import tab with name, description, source path, and folder browser controls. The folder browser is seeded from the last imported source folder or the managed Dedicated Server content folders (`Content/CustomWorlds`, `Content/QuickStarts`, `Content/Scenarios`) and shows those locations as shortcut chips. A sortable table lists existing templates with size and missing-world indicators plus Clone and Delete actions. Template world files are copied into managed Quasar storage via `QuasarWorldTemplateCatalog`. | @@ -122,11 +123,11 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar/Models/QuasarConfigProfile.cs](files/Quasar/Models/QuasarConfigProfile.cs.md) | [Quasar.Models](Modules/Quasar.Models.md) | class | Defines the configuration profile model for Space Engineers dedicated server instances managed by Quasar. A profile bundles world root settings, session settings, plugin selections, and mod selections that can be applied to one or more instances. Also defines the `QuasarNetworkType` enum with its custom JSON converter, and supporting sub-models for plugins, mods, catalog entries, and dev-folder selections. | | [Quasar/Models/QuasarRestoreReport.cs](files/Quasar/Models/QuasarRestoreReport.cs.md) | [Quasar.Models](Modules/Quasar.Models.md) | class | Outcome of a restore attempt, surfaced to the Backup page. | | [Quasar/Models/QuasarWorldTemplate.cs](files/Quasar/Models/QuasarWorldTemplate.cs.md) | [Quasar.Models](Modules/Quasar.Models.md) | class | Minimal DTO representing a world template entry in the Quasar catalog. A world template is a named reference (with optional description) to a pre-configured world that can be assigned to one or more server instances via `WorldTemplateId`. | -| [Quasar/Program.cs](files/Quasar/Program.cs.md) | [Quasar.Host](Modules/Quasar.Host.md) | class | The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. | +| [Quasar/Program.cs](files/Quasar/Program.cs.md) | [Quasar.Host](Modules/Quasar.Host.md) | class | The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, logs Quasar startup version/host/data-directory details, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. | | [Quasar/Properties/launchSettings.json](files/Quasar/Properties/launchSettings.json.md) | [Quasar.Host](Modules/Quasar.Host.md) | JSON config | Visual Studio / `dotnet run` launch profile configuration. Defines a single `http` profile for local development: runs the project directly (no IIS), binds to `http://0.0.0.0:8080`, and sets `ASPNETCORE_ENVIRONMENT=Development`. Browser auto-launch is disabled. | | [Quasar/Quasar.csproj](files/Quasar/Quasar.csproj.md) | [Quasar.Host](Modules/Quasar.Host.md) | project file | MSBuild project file for the Quasar Blazor Server host. Targets `net10.0` using the `Microsoft.NET.Sdk.Web` SDK, references the shared `Magnetar.Protocol` project, and declares NuGet packages for Steam auth, local storage, Discord, MudBlazor, NLog, SharpCompress, and a private build-only Harmony path reference. Includes custom build targets to compile `Quasar.Agent` and stage its DLLs plus runtime-specific Harmony DLLs alongside the host output. | | [Quasar/Services/AgentRegistry.cs](files/Quasar/Services/AgentRegistry.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Thread-safe, in-memory registry of all connected Quasar.Agent instances. It tracks connection state (hello, snapshot, profiler timing, command results) for every agent WebSocket session, routes outbound commands via per-agent sender delegates, and surfaces observable runtime state through a `Changed` event. It is the canonical source of live agent data consumed by the supervisor and UI. `AgentRuntimeState` is the companion per-agent mutable state bag. | -| [Quasar/Services/AgentSocketHandler.cs](files/Quasar/Services/AgentSocketHandler.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, and marks the connection disconnected in the registry on teardown. It is the bridge between the raw WebSocket transport and the rest of the supervisor stack. | +| [Quasar/Services/AgentSocketHandler.cs](files/Quasar/Services/AgentSocketHandler.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, handles agent-originated admin stop/restart lifecycle signals, and marks the connection disconnected in the registry on teardown. | | [Quasar/Services/Analytics/AnalyticsMetrics.cs](files/Quasar/Services/Analytics/AnalyticsMetrics.cs.md) | [Quasar.Services.Analytics](Modules/Quasar.Services.Analytics.md) | record + class | Central catalogue of analytics chart panels exposed by the `/analytics` dashboard and `/api/analytics/series` endpoint. Scalar entries define metric key, panel title/subtitle, sample selector, availability check, axis formatting, and fixed/dynamic Y-axis behaviour. Profiler entries define simple game-loop timing selectors and extensive-profiler top-entry selectors while keeping browser chart rendering on the same existing payload shape. | | [Quasar/Services/Analytics/AnalyticsSeriesService.cs](files/Quasar/Services/Analytics/AnalyticsSeriesService.cs.md) | [Quasar.Services.Analytics](Modules/Quasar.Services.Analytics.md) | class + records | Builds compact chart payloads for the analytics HTTP endpoint. It reads scalar metric samples from `MetricsStoreService` and profiler timing windows from `ProfilerStoreService`, buckets each selected server onto a shared timeline, flattens deep-profiler grid/entity top lists into bounded chart series, and returns aligned arrays for browser-side uPlot rendering. | | [Quasar/Services/Analytics/AnalyticsStoreOptions.cs](files/Quasar/Services/Analytics/AnalyticsStoreOptions.cs.md) | [Quasar.Services.Analytics](Modules/Quasar.Services.Analytics.md) | class | Configuration object holding the analytics persistence policy shared by `MetricsStoreService` and `ServerMetricsStore`. It resolves a retention window (in days) from environment/appsettings, constrains it to a fixed allowed set, and derives the rollup buffer capacities from it. | @@ -158,7 +159,7 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar/Services/BrowserLauncher.cs](files/Quasar/Services/BrowserLauncher.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `BrowserLauncher` is a static helper that decides whether to open a browser on startup and cross-platform launches the system default browser at a given URL. On Linux it requires a display server (`DISPLAY` or `WAYLAND_DISPLAY`) to be available. | | [Quasar/Services/DedicatedServerCatalog.cs](files/Quasar/Services/DedicatedServerCatalog.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `DedicatedServerCatalog` is the authoritative, persisted registry of all `DedicatedServerDefinition` entries managed by Quasar. It loads definitions from `server.json` files on disk at startup, watches the directory for external edits (debounced 250 ms reload), provides thread-safe upsert/delete with atomic file writes, maintains a history archive of every change, and fires a `Changed` event consumed by the supervisor and UI. | | [Quasar/Services/DedicatedServerRuntimePreparer.cs](files/Quasar/Services/DedicatedServerRuntimePreparer.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `DedicatedServerRuntimePreparer` transforms a `DedicatedServerDefinition` into a fully staged on-disk runtime immediately before a dedicated server process is launched. It writes the runtime DS config XML including the per-server advertised `ServerName`/`WorldName`, the Magnetar plugin sources/profile XML, the world `Sandbox_config.sbc` session settings and mod list, and the `LastSession.sbl` pointer file; deploys the bundled Quasar.Agent DLLs plus runtime-specific Harmony dependency; exposes bundled-vs-deployed agent hash comparison for manual refresh warnings; seeds the world from a template if needed; and computes the final command-line arguments string. The output is a `PreparedDedicatedServerLaunch` record. | -| [Quasar/Services/DedicatedServerSupervisor.cs](files/Quasar/Services/DedicatedServerSupervisor.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, and coordinates graceful stop (save + stop commands to the agent before kill) plus scheduled and maximum-uptime restarts. | +| [Quasar/Services/DedicatedServerSupervisor.cs](files/Quasar/Services/DedicatedServerSupervisor.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, coordinates graceful stop (save + stop commands to the agent before kill), and tracks admin-requested `!restart` as a supervisor-owned restart instead of a crash/reconnect wait. | | [Quasar/Services/Discord/DeathMessagesCatalog.cs](files/Quasar/Services/Discord/DeathMessagesCatalog.cs.md) | [Quasar.Services.Discord](Modules/Quasar.Services.Discord.md) | class | Singleton service that manages the persisted death-message configuration (`death-messages.json`), providing thread-safe read access, atomic saves, reset-to-defaults, and automatic hot-reload when the file is changed externally. | | [Quasar/Services/Discord/DeathMessagesConfig.cs](files/Quasar/Services/Discord/DeathMessagesConfig.cs.md) | [Quasar.Services.Discord](Modules/Quasar.Services.Discord.md) | class | Data model holding per-death-type message template lists for the Discord death-relay feature. Provides a `GetRandomMessage` picker and factory/clone helpers used by `DeathMessagesCatalog`. | | [Quasar/Services/Discord/DiscordAnalyticsExportService.cs](files/Quasar/Services/Discord/DiscordAnalyticsExportService.cs.md) | [Quasar.Services.Discord](Modules/Quasar.Services.Discord.md) | class | Periodically exports per-server analytics as Discord embeds to configured analytics channels. One background loop runs per enabled Discord server entry; each loop reads 1-minute metric samples from the metrics store and posts a rich embed with simspeed, CPU, memory, player, PCU, grid, and entity counts. | @@ -176,9 +177,9 @@ Every documented source file (209 total), alphabetical by path. See the [TOC](TO | [Quasar/Services/FileBrowserService.cs](files/Quasar/Services/FileBrowserService.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `FileBrowserService` provides server-side directory listing, shortcut generation, and breadcrumb computation for the world-path picker UI. It identifies Space Engineers world folders by the presence of `Sandbox.sbc` and offers well-known shortcuts to SE save locations. | | [Quasar/Services/IdentifierSlug.cs](files/Quasar/Services/IdentifierSlug.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | utility | Static slug helper for turning human-readable names into stable lowercase identifiers. It normalizes letters and digits, collapses whitespace, underscores, and hyphens into single hyphens, drops unsupported characters, trims edge hyphens, and can generate a unique slug by appending an incrementing numeric suffix. | | [Quasar/Services/KnownPlayerCatalog.cs](files/Quasar/Services/KnownPlayerCatalog.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `KnownPlayerCatalog` accumulates and persists a historical record of every human player seen across all managed dedicated servers. It is updated from `AgentSnapshot` telemetry and from successful command outcomes (ban/unban/promote/demote), deduplicates by `{uniqueName}::{steamId}` key, prunes snapshot-reported hidden NPC/bot player ids, automatically removes records older than the configured retention window (default 30 days by `LastSeenUtc`), saves players to `known-players.json` with a 500 ms debounce, and persists retention settings in `known-player-settings.json`. | -| [Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs](files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It also exposes a startup readiness workflow that reports SteamCMD, Magnetar, and Dedicated Server check/download/install progress before managed launches are allowed. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time and background checks compare the installed GitHub release tag + asset name with the latest full release and skip archive downloads when that identity is unchanged. Successful GitHub release resolutions are cached in memory for five minutes so a burst of managed server starts does not repeatedly call GitHub. The Magnetar install path branches by OS: Windows ships both runtime builds (`MagnetarInterim.exe` on .NET 10 and `MagnetarLegacy.exe` on .NET Framework 4.8) side-by-side and honors the per-server `DedicatedServerDefinition.ManagedRuntime` selection, while Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. | +| [Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs](files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It exposes startup readiness plus separate manual/current checks for Magnetar and the Dedicated Server, reports installed versions/paths to the warmup/update UI, and includes versions in progress events. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time/background checks compare installed GitHub release tag + asset name with the latest full release and skip archive downloads when unchanged. Successful GitHub release resolutions are cached in memory for five minutes. Windows ships both runtime builds side-by-side; Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. | | [Quasar/Services/ManagedRuntimeOptions.cs](files/Quasar/Services/ManagedRuntimeOptions.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `ManagedRuntimeOptions` is the configuration record for all managed-runtime paths and download URLs. It is populated from environment variables (highest priority), then the `Quasar:ManagedRuntime` config section, then sensible defaults. The defaults resolve the latest full Magnetar GitHub release asset by OS-specific wildcard and point SteamCMD to Valve's CDN. | -| [Quasar/Services/ManagedRuntimeWarmupService.cs](files/Quasar/Services/ManagedRuntimeWarmupService.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until those prerequisites are ready. After the startup warmup, it checks the managed Magnetar install for updates every hour and feeds Magnetar checking/download/install progress into the visible component snapshot. It exposes a component-level `ManagedRuntimeWarmupSnapshot` for dashboard progress display and fires a `Changed` event on every status update. | +| [Quasar/Services/ManagedRuntimeWarmupService.cs](files/Quasar/Services/ManagedRuntimeWarmupService.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until prerequisites are ready. After startup it checks the managed Magnetar install for updates every hour, exposes manual Magnetar and Dedicated Server check methods for the Updates page, enriches snapshots with installed versions/paths, and fires `Changed` on every status update. | | [Quasar/Services/PluginCatalogRefreshService.cs](files/Quasar/Services/PluginCatalogRefreshService.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | Background hosted service that periodically refreshes the Quasar plugin catalog so its cached plugin/manifest data stays current without user action. After a short startup delay it refreshes once, then refreshes on a fixed interval. Refresh failures are logged and the previously cached catalog is kept as a fallback. | | [Quasar/Services/PluginManifestReader.cs](files/Quasar/Services/PluginManifestReader.cs.md) | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | class | `PluginManifestReader` is a static utility for validating and reading metadata from a Magnetar plugin's manifest XML file when an admin registers a dev folder. It checks file existence and XML well-formedness, and extracts display fields (`FriendlyName`, `Author`, `Description`, `Tooltip`, `Runtimes`). | | [Quasar/Services/PluginSdk/PluginConfigDtos.cs](files/Quasar/Services/PluginSdk/PluginConfigDtos.cs.md) | [Quasar.Services.PluginSdk](Modules/Quasar.Services.PluginSdk.md) | class | Quasar-side POCOs that mirror the `ConfigStorage.SaveJson` envelope and `ConfigSchema` document produced by Magnetar's PluginSdk. These DTOs allow the Blazor config editor to deserialise and render a plugin's schema-driven UI without taking a direct dependency on the PluginSdk assembly. Field names match the SDK's camelCase JSON and are bound case-insensitively by `System.Text.Json` (Web defaults). | diff --git a/Docs/Reference/Modules/Magnetar.Protocol.md b/Docs/Reference/Modules/Magnetar.Protocol.md index 13d8d28..9942588 100644 --- a/Docs/Reference/Modules/Magnetar.Protocol.md +++ b/Docs/Reference/Modules/Magnetar.Protocol.md @@ -1,6 +1,6 @@ # Magnetar.Protocol — Shared Wire Contracts -*Module `Magnetar.Protocol` — 29 files.* See the [handbook TOC](../TOC.md) and the [file Index](../Index.md). +*Module `Magnetar.Protocol` — 30 files.* See the [handbook TOC](../TOC.md) and the [file Index](../Index.md). Shared `netstandard2.0` contract library referenced by the Quasar supervisor, the in-DS [Quasar.Agent](Quasar.Agent.md), and [Quasar.Bootstrap](Quasar.Bootstrap.md). It defines the entire agent↔supervisor wire protocol: the tagged-union `AgentWireMessage` envelope with `WireMessageKind` discriminators, the server-command request/response triple (`ServerCommandEnvelope` / `ServerCommandResult` / `ServerCommandType`), the handshake (`AgentHello`) and periodic telemetry (`AgentSnapshot` carrying `ServerMetrics`, `PlayerSnapshot`, chat/death events, PluginSdk command suggestions, and plugin info), the entity-browser and plugin-config DTOs, the `WebServiceDiscoveryManifest` used to locate a running supervisor, the `IQuasarConfigProvider` bridge, and runtime helpers (`MagnetarPaths`, `QuasarActiveReleasePointer`, `QuasarReleaseVersion`, `QuasarWebReleaseLayout`). It has zero external dependencies by design so it can also load inside the .NET-Framework game process. @@ -13,6 +13,7 @@ Shared `netstandard2.0` contract library referenced by the Quasar supervisor, th | [Magnetar.Protocol/Magnetar.Protocol.csproj](../files/Magnetar.Protocol/Magnetar.Protocol.csproj.md) | project file | MSBuild project file for the `Magnetar.Protocol` shared contract library. Targets `netstandard2.0` so the assembly can be loaded by both the Quasar Blazor Server supervisor (net8+) and the in-DS `Quasar.Agent` plugin (which runs inside the Space Engineers process). | | [Magnetar.Protocol/Model/AgentHello.cs](../files/Magnetar.Protocol/Model/AgentHello.cs.md) | class | Handshake payload sent by `Quasar.Agent` to the Quasar supervisor immediately after the WebSocket connection is established. Carries all static identity information needed by the supervisor to register the agent connection. | | [Magnetar.Protocol/Model/AgentSnapshot.cs](../files/Magnetar.Protocol/Model/AgentSnapshot.cs.md) | class | Periodic snapshot pushed by `Quasar.Agent` to the Quasar supervisor containing the full observable state of one running SE dedicated server: identity fields, runtime status, scalar performance metrics, current profiler mode, optional profiler timing data, online human players, hidden NPC/bot player ids, kicked players (serving a kick cooldown), recent chat, registered PluginSdk chat commands, recent deaths, and loaded plugin list. | +| [Magnetar.Protocol/Model/ChatCommandSnapshot.cs](../files/Magnetar.Protocol/Model/ChatCommandSnapshot.cs.md) | class | DTO describing one registered PluginSdk chat command reported by `Quasar.Agent` in `AgentSnapshot.ChatCommands`. Carries the full command text (`!prefix path`), generated syntax including arguments, help/description text, owner id, root title, minimum promote level, and path segments so Quasar can offer command autocomplete without referencing `PluginSdk`. | | [Magnetar.Protocol/Model/ChatMessageSnapshot.cs](../files/Magnetar.Protocol/Model/ChatMessageSnapshot.cs.md) | class | Immutable-style DTO representing a single in-game chat message captured for transmission in `AgentSnapshot.RecentChat`, including whether the message was emitted by the dedicated server/Good.bot rather than a player. | | [Magnetar.Protocol/Model/DeathEventSnapshot.cs](../files/Magnetar.Protocol/Model/DeathEventSnapshot.cs.md) | class | Sealed DTO representing a single player death event captured for transmission in `AgentSnapshot.RecentDeaths`. Carries victim, optional killer and weapon, death classification, and timestamp. | | [Magnetar.Protocol/Model/EntityDeleteRequest.cs](../files/Magnetar.Protocol/Model/EntityDeleteRequest.cs.md) | class | Minimal request DTO carrying the target entity ID for the `ServerCommandType.DeleteEntity` command. Serialized as JSON into `ServerCommandEnvelope.Payload`. | diff --git a/Docs/Reference/Modules/Quasar.Agent.md b/Docs/Reference/Modules/Quasar.Agent.md index 123ce11..1cd2bb6 100644 --- a/Docs/Reference/Modules/Quasar.Agent.md +++ b/Docs/Reference/Modules/Quasar.Agent.md @@ -8,8 +8,8 @@ The plugin loaded inside each Space Engineers Dedicated Server (`netstandard2.0` | File | Kind | Summary | | --- | --- | --- | -| [Quasar.Agent/AdminPlugin.cs](../files/Quasar.Agent/AdminPlugin.cs.md) | class | `AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it reads `AgentOptions`, configures and applies profiler Harmony patches, registers Quasar's admin chat commands (including the root `!stop` override), builds the `GameBridge`, starts a `PluginLogOutbox` (begun before the connection so startup log lines are buffered), wires `StopCommand` to report an admin stop before `!stop` quits the server, and starts an `AgentConnection`. It drives the game-thread snapshot/profiler refresh on each `Update`, refreshes per-character death subscriptions so respawned players are re-hooked, and handles server termination by sending an `AdminStop` signal to Quasar when shutdown was admin-initiated. | -| [Quasar.Agent/AgentConnection.cs](../files/Quasar.Agent/AgentConnection.cs.md) | class | `AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends a `Hello` handshake plus periodic `Snapshot` messages, streams buffered plugin log batches from a `PluginLogOutbox`, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. | +| [Quasar.Agent/AdminPlugin.cs](../files/Quasar.Agent/AdminPlugin.cs.md) | class | `AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it logs Magnetar/Quasar.Agent versions, reads `AgentOptions`, applies profiler Harmony patches, registers Quasar's root admin chat commands (`!stop`, `!restart`, `!quit`), builds the `GameBridge`, starts `PluginLogOutbox`, wires command hooks to early `AdminStop` / `AdminRestart` signals, and starts `AgentConnection`. It drives snapshot/profiler refresh on each `Update`, refreshes death subscriptions, and reports fallback admin shutdown intent when the server terminates outside a Quasar-requested stop. | +| [Quasar.Agent/AgentConnection.cs](../files/Quasar.Agent/AgentConnection.cs.md) | class | `AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends `Hello` plus periodic `Snapshot` messages, streams buffered plugin log batches, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, sends best-effort admin stop/restart signals before command-triggered exits, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. | | [Quasar.Agent/AgentOptions.cs](../files/Quasar.Agent/AgentOptions.cs.md) | class | `AgentOptions` is a sealed configuration DTO holding connection-resilience parameters and profiler mode for the agent's WebSocket link to Quasar. Values are read from environment variables set by Quasar when it launches a managed server (or by an operator for standalone servers), with sensible defaults applied when variables are absent or invalid. | | [Quasar.Agent/AgentProfiler.cs](../files/Quasar.Agent/AgentProfiler.cs.md) | class | Continuous in-process profiler accumulator used by Harmony method patches and IL call-site transpilers. It records elapsed ticks into numeric call-site accumulators, splits main-thread vs off-thread time, and publishes the latest one-second `ProfilerSnapshot` window for inclusion in the next agent snapshot. | | [Quasar.Agent/AgentProfilerMode.cs](../files/Quasar.Agent/AgentProfilerMode.cs.md) | enum | Public enum describing the in-DS profiler patch depth selected through `AgentOptions.ProfilerMode`. | @@ -19,7 +19,7 @@ The plugin loaded inside each Space Engineers Dedicated Server (`netstandard2.0` | [Quasar.Agent/GameBridge.cs](../files/Quasar.Agent/GameBridge.cs.md) | class | `GameBridge` is the central game-thread façade for `AgentConnection`. It collects session telemetry (metrics, current profiler mode/snapshot, human players, hidden NPC/bot player ids, kicked players, chat, registered PluginSdk chat commands, deaths, plugins), builds `AgentHello` / `AgentSnapshot` wire messages, and executes server commands (chat, blocking save, save-and-stop, profiler mode change, kick, ban, promote, clear-kick-cooldown, entity list/delete) by marshalling work onto the game thread via `MySandboxGame.Invoke`. Save/stop commands route through Magnetar PluginSdk `ServerControl` so Quasar observes completed disk saves before treating the command as successful. Metrics include process CPU derived from `Process.TotalProcessorTime`, simspeed/sim CPU from `Sync`, memory, human player count, PCU, active entities/grids, total blocks, floating objects, latest world-save time, and unsaved game-time progress since the last checkpoint. It enumerates loaded plugins from `MyPlugins.Plugins` (including Pulsar child plugins) for runtime inventory, dedupes configured fallback plugin paths against loaded plugins by path stem, parent dev-folder name, manifest ``, and manifest ``, and exposes plugin configuration through `IQuasarConfigProvider` or Magnetar PluginSdk `PluginConfig` reflection. Chat history normalizes dedicated-server/Good.bot messages to author `Server` and marks `ChatMessageSnapshot.IsServerMessage`. | | [Quasar.Agent/PluginLogOutbox.cs](../files/Quasar.Agent/PluginLogOutbox.cs.md) | class | Bounded, thread-safe buffer that captures plugin log lines emitted in-process by the PluginSdk Quasar log sink and hands them to `AgentConnection` in batches for streaming to Quasar (as `PluginLogBatch` / `WireMessageKind.PluginLogs`). The buffer survives Quasar outages: lines accumulate while disconnected and are flushed on reconnect, so the supervisor's "Recent plugin logs" panel is backfilled rather than losing everything captured while Quasar was down. | | [Quasar.Agent/Quasar.Agent.csproj](../files/Quasar.Agent/Quasar.Agent.csproj.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) | class | `StopCommand` is a Quasar-owned PluginSdk command module for the root `!stop` in-game admin command. It overrides Magnetar's earlier `stop` root by being registered later from `AdminPlugin`, acknowledges the caller, reports an admin stop to Quasar through a static hook wired by `AdminPlugin`, then calls `ServerControl.SaveAndQuit()` on a worker task so the world is saved and the dedicated server process exits. | +| [Quasar.Agent/StopCommand.cs](../files/Quasar.Agent/StopCommand.cs.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) | 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. | ## Depends on diff --git a/Docs/Reference/Modules/Quasar.Components.md b/Docs/Reference/Modules/Quasar.Components.md index 55bbc81..c80dd79 100644 --- a/Docs/Reference/Modules/Quasar.Components.md +++ b/Docs/Reference/Modules/Quasar.Components.md @@ -9,7 +9,7 @@ The Blazor Server user interface, built with MudBlazor. Routable pages cover the | File | Kind | Summary | | --- | --- | --- | | [Quasar/Components/App.razor](../files/Quasar/Components/App.razor.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. | -| [Quasar/Components/Dashboard/ServerCard.razor](../files/Quasar/Components/Dashboard/ServerCard.razor.md) | Blazor component | Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. | +| [Quasar/Components/Dashboard/ServerCard.razor](../files/Quasar/Components/Dashboard/ServerCard.razor.md) | Blazor component | Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, port/direct-connect chip, config-profile chip, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. | | [Quasar/Components/Dashboard/ServerDetailPanel.razor](../files/Quasar/Components/Dashboard/ServerDetailPanel.razor.md) | Blazor component | Detail panel embedded inside `ServerCard`. When the agent snapshot is absent it shows a waiting/error message and basic process state chips. When a snapshot is present it renders live metrics chips, Refresh/Save buttons (Save disabled while the runtime is `Starting`/`Stopping`/`Restarting`), a chat broadcast field, a players table with player identity/status columns and a rightmost unlabeled action menu column, a recent-chat list, and recent command results. The Plugins chip compares loaded runtime plugins against the assigned config profile's selected plugins, displays `loaded/total`, and turns warning-colored when the loaded count differs from the configured total; if no profile is available it falls back to the aggregate agent plugin metric. The Save chip shows save-in-progress state, or the latest world-save local time plus unsaved in-game progress as `MM:SS`, with a tooltip containing the full local timestamp. Server-authored chat (`IsServerMessage`, SteamId 0, `Good.bot`, or `Server`) is displayed as `Server`. In both the snapshot-present and snapshot-absent states, an outlined "Affinity " chip (Memory icon) is shown in the metrics chip rows when `Server.CpuAffinity` is set, and mod-download failures captured from runtime output are shown as an explicit error alert. | | [Quasar/Components/Layout/BrandingHeadContent.razor](../files/Quasar/Components/Layout/BrandingHeadContent.razor.md) | Blazor component | A lightweight head-content component that reactively updates the page favicon whenever `BrandingService` fires its `Changed` event. Rendered inside `MainLayout` so the favicon tracks live branding changes without a full page reload. | | [Quasar/Components/Layout/MainLayout.razor](../files/Quasar/Components/Layout/MainLayout.razor.md) | Blazor component | Top-level application shell layout. Provides the MudBlazor theme/provider setup, theme-configured hover-list CSS variables, a responsive app bar with branding, update notification bell, theme-mode switcher, auth (login/logout) controls, and a Quasar power dialog trigger. It hosts a collapsible side drawer with `NavMenu`, the main content area that renders `@Body`, and a full-screen overlay while Quasar is restarting or shutting down. | @@ -38,7 +38,7 @@ The Blazor Server user interface, built with MudBlazor. Routable pages cover the | [Quasar/Components/Pages/Error.razor](../files/Quasar/Components/Pages/Error.razor.md) | Blazor component | Standard ASP.NET Core error page rendered at `/Error`. Displays a generic error message and, when a request or activity ID is available, shows it for diagnostics. Advises enabling the Development environment for detailed exceptions and warns against doing so in production. | | [Quasar/Components/Pages/FolderPickerDialog.razor](../files/Quasar/Components/Pages/FolderPickerDialog.razor.md) | Blazor component | General-purpose server-side folder browser dialog. Renders a navigable directory tree with breadcrumb navigation, bookmark shortcut chips, caller-supplied shortcut chips, hidden-folder toggle, quick filtering for the current folder list, and an optional world-folder validation mode. Returns the selected absolute path on confirmation. Used from `Configs.razor` (dev-folder picker), server instance editors (world-folder picker), and world-template import flows. | | [Quasar/Components/Pages/FolderPickerDialog.razor.css](../files/Quasar/Components/Pages/FolderPickerDialog.razor.css.md) | CSS | Minimal scoped stylesheet for `FolderPickerDialog.razor`. Contains a single rule that removes the default uppercase text transform from the breadcrumb navigation buttons so directory names render in their natural casing. | -| [Quasar/Components/Pages/Home.razor](../files/Quasar/Components/Pages/Home.razor.md) | Blazor component | Routable dashboard and primary server control surface at `/`. Shows a top-of-dashboard data handling consent prompt until YES/NO is stored, a five-step first-run setup wizard that auto-opens only while no dedicated server has been created yet, a Managed Runtime panel with SteamCMD, Magnetar, and Dedicated Server readiness/download/update progress, summary KPI cards (online servers, players online, health warnings, errors), an optional problem banner, and a switchable server section. The default card view renders `ServerCard`s for live operations; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated, so the landing page does not build hidden list/table DOM. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. The wizard opens full-screen page dialogs for config-template, world-template, and server creation. | +| [Quasar/Components/Pages/Home.razor](../files/Quasar/Components/Pages/Home.razor.md) | Blazor component | Routable dashboard and primary server control surface at `/`. Shows data-handling consent, the first-run setup wizard, managed-runtime readiness/update progress, KPI cards, a problem banner, and a switchable server section whose Cards/List selection persists in browser local storage. The default card view renders `ServerCard`s in the same unique-name catalog order as the list, includes a Create Server button that opens the server editor immediately through `ServersPageDialog`, and passes config-profile click callbacks to cards; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. | | [Quasar/Components/Pages/Home.razor.css](../files/Quasar/Components/Pages/Home.razor.css.md) | CSS | Scoped stylesheet for `Home.razor`. Provides layout primitives for the setup wizard step container, individual step cards, text/action column sizing, instance-status list rows within the wizard, and the Managed Runtime readiness/progress panel. | | [Quasar/Components/Pages/Hosts.razor](../files/Quasar/Components/Pages/Hosts.razor.md) | Blazor component | Source-only host summary component. It is intentionally not routable for now, so direct navigation to `/hosts` falls through to the app Not Found page. When enabled again, it shows a summary table of every host (physical or virtual machine) that has connected at least one Quasar.Agent. Rows are aggregated from `AgentRegistry` by `HostKey`, displaying the host display name, how many distinct server slots are running on it, how many of those agents are currently connected, and total players online across that host. | | [Quasar/Components/Pages/MergeWorldTemplateModsDialog.razor](../files/Quasar/Components/Pages/MergeWorldTemplateModsDialog.razor.md) | Blazor component | MudBlazor dialog used from the Configs page to merge mods from a selected world template into the current config profile. It computes a diff of new vs. already-present Workshop IDs and returns only the net-new `QuasarModSelection` list via `DialogResult.Ok`. | @@ -52,10 +52,10 @@ The Blazor Server user interface, built with MudBlazor. Routable pages cover the | [Quasar/Components/Pages/ServerDeleteDialog.razor](../files/Quasar/Components/Pages/ServerDeleteDialog.razor.md) | Blazor component | MudBlazor dialog that confirms deletion of a server definition or reports that the definition was removed while leaving the server folder on disk. It shows the affected slug and folder path, lets the user copy the folder path, and closes with `DialogResult.Ok(true)` when the destructive or acknowledgement action is accepted. | | [Quasar/Components/Pages/ServerEditorDialog.razor](../files/Quasar/Components/Pages/ServerEditorDialog.razor.md) | Blazor component | MudBlazor dialog (no `@page` route) for creating, cloning, or editing a `DedicatedServerDefinition`. It validates server identity/runtime settings, exposes per-server Space Engineers multiplayer-list server/world name overrides, opens inline config/world-template creation dialogs, consumes world-import results that include a config-profile id, can merge missing world-template mods directly into the selected config profile before save, and offers a confirmed reset-world action while editing stopped servers. | | [Quasar/Components/Pages/ServerEditorDialog.razor.css](../files/Quasar/Components/Pages/ServerEditorDialog.razor.css.md) | CSS | Scoped stylesheet for `ServerEditorDialog.razor`. Constrains the dialog's scrollable content area and styles the template-select input slots with subtle background tints and focus highlights. Previously named `ServerEditorDialog.razor.css`. | -| [Quasar/Components/Pages/Servers.razor](../files/Quasar/Components/Pages/Servers.razor.md) | Blazor component | Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel` with the server definition, runtime snapshot, and live agent data. A separate panel lists "unmanaged" agents that report in without a matching server definition. | -| [Quasar/Components/Pages/ServersPageDialog.razor](../files/Quasar/Components/Pages/ServersPageDialog.razor.md) | Blazor component | Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. | +| [Quasar/Components/Pages/Servers.razor](../files/Quasar/Components/Pages/Servers.razor.md) | Blazor component | Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status, clickable config-profile names, copyable direct-connect ports, and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel`; a separate panel lists unmanaged agents. | +| [Quasar/Components/Pages/ServersPageDialog.razor](../files/Quasar/Components/Pages/ServersPageDialog.razor.md) | Blazor component | Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. It can optionally ask the embedded Servers component to open the Create Server editor immediately. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. | | [Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor](../files/Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor.md) | Blazor component | Small MudBlazor dialog for entering or updating the Steam Web API key used server-side for Workshop search. The key is treated as a password field and returned to the caller as a trimmed string via `DialogResult.Ok(key)`. The dialog highlights the Steam developer API-key page and explains the platform-specific storage protection Quasar applies after saving. | -| [Quasar/Components/Pages/Updates.razor](../files/Quasar/Components/Pages/Updates.razor.md) | component | Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases. It shows current update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual check/stage/activate actions for the selected UI worker version, can force a detected Bootstrap launcher update to activate immediately when running under Bootstrap, displays configured GitHub release source and asset names, provides controls for including prerelease versions plus choosing automatic or manual UI staging, and renders a git-style `appsettings.json` conflict editor with a copyable conflict-file path when staging cannot auto-merge settings. | +| [Quasar/Components/Pages/Updates.razor](../files/Quasar/Components/Pages/Updates.razor.md) | component | Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases plus inspecting/updating managed runtime components. It shows current Quasar update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual Quasar check/stage/activate actions, can force a detected Bootstrap launcher update, displays installed Magnetar and Space Engineers Dedicated Server versions/paths at all times, exposes separate manual Magnetar and DS checks, provides prerelease/auto-staging controls, and renders a git-style `appsettings.json` conflict editor when staging cannot auto-merge settings. | | [Quasar/Components/Pages/WorldTemplateFromServerDialog.razor](../files/Quasar/Components/Pages/WorldTemplateFromServerDialog.razor.md) | Blazor component | Lightweight MudBlazor confirmation dialog opened by the Servers page when the user requests to create a world template from a stopped server's current world directory. Collects the template name and description, displays a copyable source world path (read-only), and returns a `TemplateRequest` record to the caller. | | [Quasar/Components/Pages/WorldTemplateQuickImportDialog.razor](../files/Quasar/Components/Pages/WorldTemplateQuickImportDialog.razor.md) | Blazor component | MudBlazor dialog for adding a Space Engineers world template without leaving the server editor. The first step is a two-tab card: Predefined Worlds lists installed Dedicated Server world/scenario templates for one-click import, while Custom Import validates a name and absolute source path and opens a `FolderPickerDialog` for path browsing. Folder browsing shows shortcut chips for managed Dedicated Server content folders (`Content/CustomWorlds`, `Content/QuickStarts`, `Content/Scenarios`) and remembers the last selected source folder. After a source is chosen, the dialog reads source-world mods from `Sandbox_config.sbc`, lets the user create a config profile, merge into an existing profile, or ignore those mods, and returns the imported template plus an optional config-profile id. | | [Quasar/Components/Pages/WorldTemplates.razor](../files/Quasar/Components/Pages/WorldTemplates.razor.md) | Blazor component | Routable page at `/world-templates` for managing reusable Space Engineers world templates. Provides a two-tab import card: a Predefined Worlds tab for one-click importing installed Dedicated Server world/scenario templates without entering a name or path, and a Custom World Import tab with name, description, source path, and folder browser controls. The folder browser is seeded from the last imported source folder or the managed Dedicated Server content folders (`Content/CustomWorlds`, `Content/QuickStarts`, `Content/Scenarios`) and shows those locations as shortcut chips. A sortable table lists existing templates with size and missing-world indicators plus Clone and Delete actions. Template world files are copied into managed Quasar storage via `QuasarWorldTemplateCatalog`. | diff --git a/Docs/Reference/Modules/Quasar.Host.md b/Docs/Reference/Modules/Quasar.Host.md index 8145755..78bf16b 100644 --- a/Docs/Reference/Modules/Quasar.Host.md +++ b/Docs/Reference/Modules/Quasar.Host.md @@ -8,7 +8,7 @@ The Blazor Server application host and composition root. `Program.cs` builds the | File | Kind | Summary | | --- | --- | --- | -| [Quasar/Program.cs](../files/Quasar/Program.cs.md) | class | The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. | +| [Quasar/Program.cs](../files/Quasar/Program.cs.md) | class | The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, logs Quasar startup version/host/data-directory details, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. | | [Quasar/Properties/launchSettings.json](../files/Quasar/Properties/launchSettings.json.md) | JSON config | Visual Studio / `dotnet run` launch profile configuration. Defines a single `http` profile for local development: runs the project directly (no IIS), binds to `http://0.0.0.0:8080`, and sets `ASPNETCORE_ENVIRONMENT=Development`. Browser auto-launch is disabled. | | [Quasar/Quasar.csproj](../files/Quasar/Quasar.csproj.md) | project file | MSBuild project file for the Quasar Blazor Server host. Targets `net10.0` using the `Microsoft.NET.Sdk.Web` SDK, references the shared `Magnetar.Protocol` project, and declares NuGet packages for Steam auth, local storage, Discord, MudBlazor, NLog, SharpCompress, and a private build-only Harmony path reference. Includes custom build targets to compile `Quasar.Agent` and stage its DLLs plus runtime-specific Harmony DLLs alongside the host output. | | [Quasar/appsettings.Development.json](../files/Quasar/appsettings.Development.json.md) | JSON config | Development-environment override for `appsettings.json`, loaded when `ASPNETCORE_ENVIRONMENT=Development`. Enables `DetailedErrors` for Blazor circuit diagnostics and carries a standard `Logging` block with the same log levels as the base file. | diff --git a/Docs/Reference/Modules/Quasar.Services.Core.md b/Docs/Reference/Modules/Quasar.Services.Core.md index 000b366..2c00151 100644 --- a/Docs/Reference/Modules/Quasar.Services.Core.md +++ b/Docs/Reference/Modules/Quasar.Services.Core.md @@ -9,7 +9,7 @@ The heart of the supervisor and its supporting services. `DedicatedServerSupervi | File | Kind | Summary | | --- | --- | --- | | [Quasar/Services/AgentRegistry.cs](../files/Quasar/Services/AgentRegistry.cs.md) | class | Thread-safe, in-memory registry of all connected Quasar.Agent instances. It tracks connection state (hello, snapshot, profiler timing, command results) for every agent WebSocket session, routes outbound commands via per-agent sender delegates, and surfaces observable runtime state through a `Changed` event. It is the canonical source of live agent data consumed by the supervisor and UI. `AgentRuntimeState` is the companion per-agent mutable state bag. | -| [Quasar/Services/AgentSocketHandler.cs](../files/Quasar/Services/AgentSocketHandler.cs.md) | class | `AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, and marks the connection disconnected in the registry on teardown. It is the bridge between the raw WebSocket transport and the rest of the supervisor stack. | +| [Quasar/Services/AgentSocketHandler.cs](../files/Quasar/Services/AgentSocketHandler.cs.md) | class | `AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, handles agent-originated admin stop/restart lifecycle signals, and marks the connection disconnected in the registry on teardown. | | [Quasar/Services/AtomicFileWriter.cs](../files/Quasar/Services/AtomicFileWriter.cs.md) | class | `AtomicFileWriter` is a static utility that writes text files atomically using the write-to-temp-then-rename pattern, ensuring the target file is never left in a partially written state. All supervisor, catalog, and branding persistence goes through this helper. | | [Quasar/Services/Backup/AutomaticBackupService.cs](../files/Quasar/Services/Backup/AutomaticBackupService.cs.md) | class | Background scheduler and manual backup queue that writes Quasar config, server, and world backups into the configured backup directory. Scheduled runs follow separate `QuasarBackupSettings` rules and prune to each rule's retention count; manual UI starts are queued so Blazor page navigation is not blocked by ZIP creation. | | [Quasar/Services/Backup/BackupCompatibility.cs](../files/Quasar/Services/Backup/BackupCompatibility.cs.md) | class (static) + record struct | Applies semantic-versioning rules deciding whether a backup may restore into the running Quasar. Same `Major.Minor` is always allowed (patch may differ in either direction); an older `Major.Minor` is allowed only if a forward migration path exists; a newer `Major.Minor` is rejected (no cross-major.minor downgrade). | @@ -22,14 +22,14 @@ The heart of the supervisor and its supporting services. `DedicatedServerSupervi | [Quasar/Services/BrowserLauncher.cs](../files/Quasar/Services/BrowserLauncher.cs.md) | class | `BrowserLauncher` is a static helper that decides whether to open a browser on startup and cross-platform launches the system default browser at a given URL. On Linux it requires a display server (`DISPLAY` or `WAYLAND_DISPLAY`) to be available. | | [Quasar/Services/DedicatedServerCatalog.cs](../files/Quasar/Services/DedicatedServerCatalog.cs.md) | class | `DedicatedServerCatalog` is the authoritative, persisted registry of all `DedicatedServerDefinition` entries managed by Quasar. It loads definitions from `server.json` files on disk at startup, watches the directory for external edits (debounced 250 ms reload), provides thread-safe upsert/delete with atomic file writes, maintains a history archive of every change, and fires a `Changed` event consumed by the supervisor and UI. | | [Quasar/Services/DedicatedServerRuntimePreparer.cs](../files/Quasar/Services/DedicatedServerRuntimePreparer.cs.md) | class | `DedicatedServerRuntimePreparer` transforms a `DedicatedServerDefinition` into a fully staged on-disk runtime immediately before a dedicated server process is launched. It writes the runtime DS config XML including the per-server advertised `ServerName`/`WorldName`, the Magnetar plugin sources/profile XML, the world `Sandbox_config.sbc` session settings and mod list, and the `LastSession.sbl` pointer file; deploys the bundled Quasar.Agent DLLs plus runtime-specific Harmony dependency; exposes bundled-vs-deployed agent hash comparison for manual refresh warnings; seeds the world from a template if needed; and computes the final command-line arguments string. The output is a `PreparedDedicatedServerLaunch` record. | -| [Quasar/Services/DedicatedServerSupervisor.cs](../files/Quasar/Services/DedicatedServerSupervisor.cs.md) | class | `DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, and coordinates graceful stop (save + stop commands to the agent before kill) plus scheduled and maximum-uptime restarts. | +| [Quasar/Services/DedicatedServerSupervisor.cs](../files/Quasar/Services/DedicatedServerSupervisor.cs.md) | class | `DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, coordinates graceful stop (save + stop commands to the agent before kill), and tracks admin-requested `!restart` as a supervisor-owned restart instead of a crash/reconnect wait. | | [Quasar/Services/EntityService.cs](../files/Quasar/Services/EntityService.cs.md) | class | `EntityService` issues live entity queries and deletions to a connected Quasar.Agent by routing request/response commands through `AgentRegistry.SendCommandAndWaitAsync`. It serialises filter/request payloads to JSON and deserialises the agent's result payload back to typed models. | | [Quasar/Services/FileBrowserService.cs](../files/Quasar/Services/FileBrowserService.cs.md) | class | `FileBrowserService` provides server-side directory listing, shortcut generation, and breadcrumb computation for the world-path picker UI. It identifies Space Engineers world folders by the presence of `Sandbox.sbc` and offers well-known shortcuts to SE save locations. | | [Quasar/Services/IdentifierSlug.cs](../files/Quasar/Services/IdentifierSlug.cs.md) | utility | Static slug helper for turning human-readable names into stable lowercase identifiers. It normalizes letters and digits, collapses whitespace, underscores, and hyphens into single hyphens, drops unsupported characters, trims edge hyphens, and can generate a unique slug by appending an incrementing numeric suffix. | | [Quasar/Services/KnownPlayerCatalog.cs](../files/Quasar/Services/KnownPlayerCatalog.cs.md) | class | `KnownPlayerCatalog` accumulates and persists a historical record of every human player seen across all managed dedicated servers. It is updated from `AgentSnapshot` telemetry and from successful command outcomes (ban/unban/promote/demote), deduplicates by `{uniqueName}::{steamId}` key, prunes snapshot-reported hidden NPC/bot player ids, automatically removes records older than the configured retention window (default 30 days by `LastSeenUtc`), saves players to `known-players.json` with a 500 ms debounce, and persists retention settings in `known-player-settings.json`. | -| [Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs](../files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md) | class | `ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It also exposes a startup readiness workflow that reports SteamCMD, Magnetar, and Dedicated Server check/download/install progress before managed launches are allowed. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time and background checks compare the installed GitHub release tag + asset name with the latest full release and skip archive downloads when that identity is unchanged. Successful GitHub release resolutions are cached in memory for five minutes so a burst of managed server starts does not repeatedly call GitHub. The Magnetar install path branches by OS: Windows ships both runtime builds (`MagnetarInterim.exe` on .NET 10 and `MagnetarLegacy.exe` on .NET Framework 4.8) side-by-side and honors the per-server `DedicatedServerDefinition.ManagedRuntime` selection, while Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. | +| [Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs](../files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md) | class | `ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It exposes startup readiness plus separate manual/current checks for Magnetar and the Dedicated Server, reports installed versions/paths to the warmup/update UI, and includes versions in progress events. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time/background checks compare installed GitHub release tag + asset name with the latest full release and skip archive downloads when unchanged. Successful GitHub release resolutions are cached in memory for five minutes. Windows ships both runtime builds side-by-side; Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. | | [Quasar/Services/ManagedRuntimeOptions.cs](../files/Quasar/Services/ManagedRuntimeOptions.cs.md) | class | `ManagedRuntimeOptions` is the configuration record for all managed-runtime paths and download URLs. It is populated from environment variables (highest priority), then the `Quasar:ManagedRuntime` config section, then sensible defaults. The defaults resolve the latest full Magnetar GitHub release asset by OS-specific wildcard and point SteamCMD to Valve's CDN. | -| [Quasar/Services/ManagedRuntimeWarmupService.cs](../files/Quasar/Services/ManagedRuntimeWarmupService.cs.md) | class | `ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until those prerequisites are ready. After the startup warmup, it checks the managed Magnetar install for updates every hour and feeds Magnetar checking/download/install progress into the visible component snapshot. It exposes a component-level `ManagedRuntimeWarmupSnapshot` for dashboard progress display and fires a `Changed` event on every status update. | +| [Quasar/Services/ManagedRuntimeWarmupService.cs](../files/Quasar/Services/ManagedRuntimeWarmupService.cs.md) | class | `ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until prerequisites are ready. After startup it checks the managed Magnetar install for updates every hour, exposes manual Magnetar and Dedicated Server check methods for the Updates page, enriches snapshots with installed versions/paths, and fires `Changed` on every status update. | | [Quasar/Services/PluginCatalogRefreshService.cs](../files/Quasar/Services/PluginCatalogRefreshService.cs.md) | class | Background hosted service that periodically refreshes the Quasar plugin catalog so its cached plugin/manifest data stays current without user action. After a short startup delay it refreshes once, then refreshes on a fixed interval. Refresh failures are logged and the previously cached catalog is kept as a fallback. | | [Quasar/Services/PluginManifestReader.cs](../files/Quasar/Services/PluginManifestReader.cs.md) | class | `PluginManifestReader` is a static utility for validating and reading metadata from a Magnetar plugin's manifest XML file when an admin registers a dev folder. It checks file existence and XML well-formedness, and extracts display fields (`FriendlyName`, `Author`, `Description`, `Tooltip`, `Runtimes`). | | [Quasar/Services/QuasarConfigMetadata.cs](../files/Quasar/Services/QuasarConfigMetadata.cs.md) | class | Defines the full compile-time schema for every Quasar configuration option that the UI can present and edit. It declares enums for option scope (`Root`/`Session`) and kind (Boolean, Integer, Decimal, Text, LongText, Password, KeyValueText, SelectInteger, SelectText), typed record helpers for categories and select options, and a rich `QuasarConfigOptionDefinition` type with validation and full-text search. The static `QuasarConfigMetadata` class holds two read-only lists — `Categories` (12 groupings, including Survival) and `Options` (covering Quasar root settings plus DS-visible session settings, including block type limits) — and provides reflection-based property lookup and value formatting helpers used by the config editor UI. | diff --git a/Docs/Reference/TOC.md b/Docs/Reference/TOC.md index e7d70fc..c4f3926 100644 --- a/Docs/Reference/TOC.md +++ b/Docs/Reference/TOC.md @@ -4,7 +4,7 @@ Generated reference handbook for the **Quasar** stack — a supervisor and manag For the hand-written architecture narrative and design rationale, see [QuasarArchitecture.md](../QuasarArchitecture.md). -This handbook covers **209 source files** across **11 modules**. Browse by module below, or jump to the flat [file Index](Index.md). +This handbook covers **210 source files** across **11 modules**. Browse by module below, or jump to the flat [file Index](Index.md). ## Runtime topology @@ -21,7 +21,7 @@ This handbook covers **209 source files** across **11 modules**. Browse by modul | Module | Files | Summary | | --- | --- | --- | -| [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | 29 | Shared wire/discovery contracts and release/runtime helpers between agent and supervisor. | +| [Magnetar.Protocol](Modules/Magnetar.Protocol.md) | 30 | Shared wire/discovery contracts and release/runtime helpers between agent and supervisor. | | [Quasar.Host](Modules/Quasar.Host.md) | 10 | Blazor Server host: DI graph, auth, endpoints, static assets. | | [Quasar.Models](Modules/Quasar.Models.md) | 15 | Domain models for instances, config profiles, templates, branding. | | [Quasar.Services.Core](Modules/Quasar.Services.Core.md) | 45 | Supervisor engine, agent registry, runtime preparation, catalogs. | diff --git a/Docs/Reference/data/manifest.json b/Docs/Reference/data/manifest.json index abb308b..2723f9d 100644 --- a/Docs/Reference/data/manifest.json +++ b/Docs/Reference/data/manifest.json @@ -50,6 +50,16 @@ "tier": 1, "status": "pending" }, + { + "path": "Magnetar.Protocol/Model/ChatCommandSnapshot.cs", + "name": "ChatCommandSnapshot.cs", + "ext": ".cs", + "size": 701, + "sha256": "c4eb5043fa1656cb1ad43dcf2bddfadab64498d627140404c42e43754214827e", + "module": "Magnetar.Protocol", + "tier": 1, + "status": "pending" + }, { "path": "Magnetar.Protocol/Model/ChatMessageSnapshot.cs", "name": "ChatMessageSnapshot.cs", @@ -284,8 +294,8 @@ "path": "Magnetar.Protocol/Transport/WireMessageKind.cs", "name": "WireMessageKind.cs", "ext": ".cs", - "size": 593, - "sha256": "458713f7647abbcd4096ed2be47fe9a4ec696dbcf81c9ac668f78a33e73ae3ee", + "size": 649, + "sha256": "4b81f9acecb2078f004e3e65b2f280577b818f68314b3d2bf248d2c2b24a197f", "module": "Magnetar.Protocol", "tier": 1, "status": "pending" @@ -294,8 +304,8 @@ "path": "Quasar.Agent/AdminPlugin.cs", "name": "AdminPlugin.cs", "ext": ".cs", - "size": 9315, - "sha256": "777ed6a9d842f4fe296540a1c44f94c1d29713fb217d2c88ffdf784986a8b778", + "size": 13494, + "sha256": "4816f048c09a4db6adc4cb7cbe06d1d559985d9c628933caa052e6adbaefad1d", "module": "Quasar.Agent", "tier": 1, "status": "pending" @@ -304,8 +314,8 @@ "path": "Quasar.Agent/AgentConnection.cs", "name": "AgentConnection.cs", "ext": ".cs", - "size": 16153, - "sha256": "1462db30d6af7860f3c546497bff59f7518cc404a7b5bf1980a1206f01feffa8", + "size": 16689, + "sha256": "d519d5d4b9541988ead67cb467993d4fba0b08524451d56c2e442d6e491d6fe6", "module": "Quasar.Agent", "tier": 1, "status": "pending" @@ -404,8 +414,8 @@ "path": "Quasar.Agent/StopCommand.cs", "name": "StopCommand.cs", "ext": ".cs", - "size": 878, - "sha256": "f300c0a40adabc1b519b321da8e00203b739d6528dea994ad54cdadc5301de1d", + "size": 2412, + "sha256": "8272c9c2c6acd0945bd1efabde87c39e41241588ef87fa6a2430f1af6fac250d", "module": "Quasar.Agent", "tier": 1, "status": "pending" @@ -464,8 +474,8 @@ "path": "Quasar/Components/Dashboard/ServerCard.razor", "name": "ServerCard.razor", "ext": ".razor", - "size": 10120, - "sha256": "5285be6c70584b93ea1ecf6a63f0a3dca4b7fd664f6332bbe69fece324d5bb01", + "size": 14212, + "sha256": "0c661afac2828fab5a872e91c4ef1cbaa82a2e2003d646d9a121e1d8235a9a0e", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -754,8 +764,8 @@ "path": "Quasar/Components/Pages/Home.razor", "name": "Home.razor", "ext": ".razor", - "size": 49988, - "sha256": "578dc72410a55feccb3c3c86442eba91180b6202e6bd8606667acc104c3226a8", + "size": 52638, + "sha256": "4a47b1ce93582da3beb03e606ff53d7408f6667b5975c5699ba0c616f4ed6540", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -894,8 +904,8 @@ "path": "Quasar/Components/Pages/Servers.razor", "name": "Servers.razor", "ext": ".razor", - "size": 39549, - "sha256": "364c91b58257ee12b85dd84826fd8be7d398d9a01f3c24af57e4c143bef68f8c", + "size": 42345, + "sha256": "fb8d5a805ff025662ec94326f1e9f1ebbd15d1b67b018ee2e244cdec4cf45872", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -904,8 +914,8 @@ "path": "Quasar/Components/Pages/ServersPageDialog.razor", "name": "ServersPageDialog.razor", "ext": ".razor", - "size": 1350, - "sha256": "0dce068bf5826c95f7941fe684a28e24798f92d22eac8a015c239a558309e40b", + "size": 1471, + "sha256": "fe4d542538d9ff85aab899ea1bdf33c3afbeeb4aeee01f467eb6e31c329c21fb", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -924,8 +934,8 @@ "path": "Quasar/Components/Pages/Updates.razor", "name": "Updates.razor", "ext": ".razor", - "size": 22308, - "sha256": "71e45775fd59dc82903dcd4f77580b72ea1b3f859535e446f58628176f350095", + "size": 27689, + "sha256": "0769fe53ceff39972594110e53a22ca0f1bf4c090ede2cdb4301a99a5b5d30b2", "module": "Quasar.Components", "tier": 2, "status": "pending" @@ -1184,8 +1194,8 @@ "path": "Quasar/Program.cs", "name": "Program.cs", "ext": ".cs", - "size": 41206, - "sha256": "da3f1b528fbf049a02045327d2813fbcd6b86fb97b684c04ef2b6d8e60246fcb", + "size": 41681, + "sha256": "7a5ae172ec8e888653289ac1d2524694c55df79152670600f35da800226d6353", "module": "Quasar.Host", "tier": 1, "status": "pending" @@ -1224,8 +1234,8 @@ "path": "Quasar/Services/AgentSocketHandler.cs", "name": "AgentSocketHandler.cs", "ext": ".cs", - "size": 8927, - "sha256": "4b5b7f75dec749714141e4aec63db69de7a4631f5a41b914baea8a4a9ee47163", + "size": 9728, + "sha256": "313389a492779ca93a6c09f3e3c0cf5774595c0b5f0017ae9e791558c17b6948", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" @@ -1544,8 +1554,8 @@ "path": "Quasar/Services/DedicatedServerSupervisor.cs", "name": "DedicatedServerSupervisor.cs", "ext": ".cs", - "size": 110154, - "sha256": "c75b130174fcc43d2fbe0f5b047f8bd66643061f73147705dba6bc16647adef6", + "size": 112485, + "sha256": "0063a05d688c7c7cced54636fa7b6e408917e6e059a600eedf4b6e214f9e5de5", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" @@ -1724,8 +1734,8 @@ "path": "Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs", "name": "ManagedDedicatedServerRuntimeResolver.cs", "ext": ".cs", - "size": 69981, - "sha256": "8e37141513322c684972f096ada9bd0dc5de977e2a58aefbe57c899831a6c59d", + "size": 77042, + "sha256": "92beddebc90450f953395900c227a50d7d0fdcceeddba141329b03d1f96e70c6", "module": "Quasar.Services.Core", "tier": 1, "status": "pending" @@ -1744,8 +1754,8 @@ "path": "Quasar/Services/ManagedRuntimeWarmupService.cs", "name": "ManagedRuntimeWarmupService.cs", "ext": ".cs", - "size": 9459, - "sha256": "1d7ac7af0aeeca0aabafe4f5237b46d1de63f9b9b83e259b621545b58e16f41d", + "size": 12645, + "sha256": "3dea44781779017c659e9b243e6f80faba2770f1538d27fb5decc3b421c19fc8", "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 be7aedc..2db90df 100644 --- a/Docs/Reference/data/module_index.json +++ b/Docs/Reference/data/module_index.json @@ -35,6 +35,13 @@ "tier": 1, "summary": "Periodic snapshot pushed by `Quasar.Agent` to the Quasar supervisor containing the full observable state of one running SE dedicated server: identity fields, runtime status, scalar performance metrics, current profiler mode, optional profiler timing data, online human players, hidden NPC/bot player ids, kicked players (serving a kick cooldown), recent chat, registered PluginSdk chat commands, recent deaths, and loaded plugin list." }, + { + "path": "Magnetar.Protocol/Model/ChatCommandSnapshot.cs", + "name": "ChatCommandSnapshot.cs", + "kind": "class", + "tier": 1, + "summary": "DTO describing one registered PluginSdk chat command reported by `Quasar.Agent` in `AgentSnapshot.ChatCommands`. Carries the full command text (`!prefix path`), generated syntax including arguments, help/description text, owner id, root title, minimum promote level, and path segments so Quasar can offer command autocomplete without referencing `PluginSdk`." + }, { "path": "Magnetar.Protocol/Model/ChatMessageSnapshot.cs", "name": "ChatMessageSnapshot.cs", @@ -210,14 +217,14 @@ "name": "AdminPlugin.cs", "kind": "class", "tier": 1, - "summary": "`AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it reads `AgentOptions`, configures and applies profiler Harmony patches, registers Quasar's admin chat commands (including the root `!stop` override), builds the `GameBridge`, starts a `PluginLogOutbox` (begun before the connection so startup log lines are buffered), wires `StopCommand` to report an admin stop before `!stop` quits the server, and starts an `AgentConnection`. It drives the game-thread snapshot/profiler refresh on each `Update`, refreshes per-character death subscriptions so respawned players are re-hooked, and handles server termination by sending an `AdminStop` signal to Quasar when shutdown was admin-initiated." + "summary": "`AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it logs Magnetar/Quasar.Agent versions, reads `AgentOptions`, applies profiler Harmony patches, registers Quasar's root admin chat commands (`!stop`, `!restart`, `!quit`), builds the `GameBridge`, starts `PluginLogOutbox`, wires command hooks to early `AdminStop` / `AdminRestart` signals, and starts `AgentConnection`. It drives snapshot/profiler refresh on each `Update`, refreshes death subscriptions, and reports fallback admin shutdown intent when the server terminates outside a Quasar-requested stop." }, { "path": "Quasar.Agent/AgentConnection.cs", "name": "AgentConnection.cs", "kind": "class", "tier": 1, - "summary": "`AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends a `Hello` handshake plus periodic `Snapshot` messages, streams buffered plugin log batches from a `PluginLogOutbox`, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window." + "summary": "`AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends `Hello` plus periodic `Snapshot` messages, streams buffered plugin log batches, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, sends best-effort admin stop/restart signals before command-triggered exits, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window." }, { "path": "Quasar.Agent/AgentOptions.cs", @@ -287,7 +294,7 @@ "name": "StopCommand.cs", "kind": "class", "tier": 1, - "summary": "`StopCommand` is a Quasar-owned PluginSdk command module for the root `!stop` in-game admin command. It overrides Magnetar's earlier `stop` root by being registered later from `AdminPlugin`, acknowledges the caller, reports an admin stop to Quasar through a static hook wired by `AdminPlugin`, then calls `ServerControl.SaveAndQuit()` on a worker task so the world is saved and the dedicated server process exits." + "summary": "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." }, { "path": "Quasar.Agent/WebServiceLocator.cs", @@ -333,7 +340,7 @@ "name": "ServerCard.razor", "kind": "Blazor component", "tier": 2, - "summary": "Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing." + "summary": "Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, port/direct-connect chip, config-profile chip, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing." }, { "path": "Quasar/Components/Dashboard/ServerDetailPanel.razor", @@ -536,7 +543,7 @@ "name": "Home.razor", "kind": "Blazor component", "tier": 2, - "summary": "Routable dashboard and primary server control surface at `/`. Shows a top-of-dashboard data handling consent prompt until YES/NO is stored, a five-step first-run setup wizard that auto-opens only while no dedicated server has been created yet, a Managed Runtime panel with SteamCMD, Magnetar, and Dedicated Server readiness/download/update progress, summary KPI cards (online servers, players online, health warnings, errors), an optional problem banner, and a switchable server section. The default card view renders `ServerCard`s for live operations; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated, so the landing page does not build hidden list/table DOM. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. The wizard opens full-screen page dialogs for config-template, world-template, and server creation." + "summary": "Routable dashboard and primary server control surface at `/`. Shows data-handling consent, the first-run setup wizard, managed-runtime readiness/update progress, KPI cards, a problem banner, and a switchable server section whose Cards/List selection persists in browser local storage. The default card view renders `ServerCard`s in the same unique-name catalog order as the list, includes a Create Server button that opens the server editor immediately through `ServersPageDialog`, and passes config-profile click callbacks to cards; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation." }, { "path": "Quasar/Components/Pages/Home.razor.css", @@ -634,14 +641,14 @@ "name": "Servers.razor", "kind": "Blazor component", "tier": 2, - "summary": "Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel` with the server definition, runtime snapshot, and live agent data. A separate panel lists \"unmanaged\" agents that report in without a matching server definition." + "summary": "Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status, clickable config-profile names, copyable direct-connect ports, and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel`; a separate panel lists unmanaged agents." }, { "path": "Quasar/Components/Pages/ServersPageDialog.razor", "name": "ServersPageDialog.razor", "kind": "Blazor component", "tier": 2, - "summary": "Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place." + "summary": "Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. It can optionally ask the embedded Servers component to open the Create Server editor immediately. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place." }, { "path": "Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor", @@ -655,7 +662,7 @@ "name": "Updates.razor", "kind": "component", "tier": 2, - "summary": "Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases. It shows current update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual check/stage/activate actions for the selected UI worker version, can force a detected Bootstrap launcher update to activate immediately when running under Bootstrap, displays configured GitHub release source and asset names, provides controls for including prerelease versions plus choosing automatic or manual UI staging, and renders a git-style `appsettings.json` conflict editor with a copyable conflict-file path when staging cannot auto-merge settings." + "summary": "Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases plus inspecting/updating managed runtime components. It shows current Quasar update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual Quasar check/stage/activate actions, can force a detected Bootstrap launcher update, displays installed Magnetar and Space Engineers Dedicated Server versions/paths at all times, exposes separate manual Magnetar and DS checks, provides prerelease/auto-staging controls, and renders a git-style `appsettings.json` conflict editor when staging cannot auto-merge settings." }, { "path": "Quasar/Components/Pages/WorldTemplateFromServerDialog.razor", @@ -841,7 +848,7 @@ "name": "Program.cs", "kind": "class", "tier": 1, - "summary": "The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, and runs the app. It is the system wiring hub \u2014 essentially every service in the process is registered here." + "summary": "The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, logs Quasar startup version/host/data-directory details, and runs the app. It is the system wiring hub \u2014 essentially every service in the process is registered here." }, { "path": "Quasar/Properties/launchSettings.json", @@ -920,7 +927,7 @@ "name": "AgentSocketHandler.cs", "kind": "class", "tier": 1, - "summary": "`AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, and marks the connection disconnected in the registry on teardown. It is the bridge between the raw WebSocket transport and the rest of the supervisor stack." + "summary": "`AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, handles agent-originated admin stop/restart lifecycle signals, and marks the connection disconnected in the registry on teardown." }, { "path": "Quasar/Services/AtomicFileWriter.cs", @@ -1011,7 +1018,7 @@ "name": "DedicatedServerSupervisor.cs", "kind": "class", "tier": 1, - "summary": "`DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, and coordinates graceful stop (save + stop commands to the agent before kill) plus scheduled and maximum-uptime restarts." + "summary": "`DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, coordinates graceful stop (save + stop commands to the agent before kill), and tracks admin-requested `!restart` as a supervisor-owned restart instead of a crash/reconnect wait." }, { "path": "Quasar/Services/EntityService.cs", @@ -1046,7 +1053,7 @@ "name": "ManagedDedicatedServerRuntimeResolver.cs", "kind": "class", "tier": 1, - "summary": "`ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server \u2014 the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process \u2014 and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It also exposes a startup readiness workflow that reports SteamCMD, Magnetar, and Dedicated Server check/download/install progress before managed launches are allowed. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time and background checks compare the installed GitHub release tag + asset name with the latest full release and skip archive downloads when that identity is unchanged. Successful GitHub release resolutions are cached in memory for five minutes so a burst of managed server starts does not repeatedly call GitHub. The Magnetar install path branches by OS: Windows ships both runtime builds (`MagnetarInterim.exe` on .NET 10 and `MagnetarLegacy.exe` on .NET Framework 4.8) side-by-side and honors the per-server `DedicatedServerDefinition.ManagedRuntime` selection, while Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`." + "summary": "`ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server \u2014 the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process \u2014 and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It exposes startup readiness plus separate manual/current checks for Magnetar and the Dedicated Server, reports installed versions/paths to the warmup/update UI, and includes versions in progress events. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time/background checks compare installed GitHub release tag + asset name with the latest full release and skip archive downloads when unchanged. Successful GitHub release resolutions are cached in memory for five minutes. Windows ships both runtime builds side-by-side; Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`." }, { "path": "Quasar/Services/ManagedRuntimeOptions.cs", @@ -1060,7 +1067,7 @@ "name": "ManagedRuntimeWarmupService.cs", "kind": "class", "tier": 1, - "summary": "`ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until those prerequisites are ready. After the startup warmup, it checks the managed Magnetar install for updates every hour and feeds Magnetar checking/download/install progress into the visible component snapshot. It exposes a component-level `ManagedRuntimeWarmupSnapshot` for dashboard progress display and fires a `Changed` event on every status update." + "summary": "`ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until prerequisites are ready. After startup it checks the managed Magnetar install for updates every hour, exposes manual Magnetar and Dedicated Server check methods for the Updates page, enriches snapshots with installed versions/paths, and fires `Changed` on every status update." }, { "path": "Quasar/Services/PluginCatalogRefreshService.cs", diff --git a/Docs/Reference/data/reference_graph.json b/Docs/Reference/data/reference_graph.json index 28cfc38..45310b3 100644 --- a/Docs/Reference/data/reference_graph.json +++ b/Docs/Reference/data/reference_graph.json @@ -8,6 +8,7 @@ "Magnetar.Protocol/Transport/AgentWireMessage.cs" ], "Magnetar.Protocol/Model/AgentSnapshot.cs": [ + "Magnetar.Protocol/Model/ChatCommandSnapshot.cs", "Magnetar.Protocol/Model/ChatMessageSnapshot.cs", "Magnetar.Protocol/Model/DeathEventSnapshot.cs", "Magnetar.Protocol/Model/KickedPlayerSnapshot.cs", @@ -17,6 +18,9 @@ "Magnetar.Protocol/Model/ServerMetrics.cs", "Magnetar.Protocol/Transport/AgentWireMessage.cs" ], + "Magnetar.Protocol/Model/ChatCommandSnapshot.cs": [ + "Magnetar.Protocol/Model/AgentSnapshot.cs" + ], "Magnetar.Protocol/Model/ChatMessageSnapshot.cs": [ "Magnetar.Protocol/Model/AgentSnapshot.cs" ], @@ -175,6 +179,7 @@ ], "Quasar/Components/Dashboard/ServerCard.razor": [ "Quasar/Components/Dashboard/ServerDetailPanel.razor", + "Quasar/Services/QuasarConfigProfileCatalog.cs", "Quasar/Services/ServerManagementActions.cs" ], "Quasar/Components/Dashboard/ServerDetailPanel.razor": [ @@ -245,6 +250,7 @@ "Quasar/Services/WebServiceOptions.cs" ], "Quasar/Components/Pages/Chat.razor": [ + "Magnetar.Protocol/Model/ChatCommandSnapshot.cs", "Magnetar.Protocol/Model/ChatMessageSnapshot.cs", "Magnetar.Protocol/Transport/ServerCommandEnvelope.cs", "Magnetar.Protocol/Transport/ServerCommandType.cs", @@ -301,6 +307,7 @@ "Quasar/Components/Pages/FolderPickerDialog.razor" ], "Quasar/Components/Pages/Home.razor": [ + "Quasar/Components/Dashboard/ServerCard.razor", "Quasar/Components/Pages/ConfigsPageDialog.razor", "Quasar/Components/Pages/Servers.razor", "Quasar/Components/Shared/CopyablePath.razor", @@ -384,6 +391,7 @@ "Quasar/Components/Pages/SteamWorkshopApiKeyDialog.razor": [], "Quasar/Components/Pages/Updates.razor": [ "Quasar/Components/Shared/CopyablePath.razor", + "Quasar/Services/ManagedRuntimeWarmupService.cs", "Quasar/Services/Updates/QuasarUpdateOptions.cs", "Quasar/Services/Updates/QuasarUpdateService.cs", "Quasar/Services/Updates/QuasarUpdateSnapshot.cs", diff --git a/Docs/Reference/files/Magnetar.Protocol/Transport/WireMessageKind.cs.md b/Docs/Reference/files/Magnetar.Protocol/Transport/WireMessageKind.cs.md index 196292f..161a3cc 100644 --- a/Docs/Reference/files/Magnetar.Protocol/Transport/WireMessageKind.cs.md +++ b/Docs/Reference/files/Magnetar.Protocol/Transport/WireMessageKind.cs.md @@ -19,10 +19,11 @@ Namespace `Magnetar.Protocol.Transport`; `public static class WireMessageKind`. | `PluginConfigSnapshot` | `"plugin-config-snapshot"` | Agent→Quasar | Current plugin config state. | | `PluginConfigUpdate` | `"plugin-config-update"` | Quasar→Agent | Apply updated plugin config values. | | `AdminStop` | `"admin-stop"` | Agent→Quasar | Admin/console-initiated stop Quasar did not request. | +| `AdminRestart` | `"admin-restart"` | Agent→Quasar | Admin-initiated in-game restart Quasar should track and relaunch. | | `PluginLogs` | `"plugin-logs"` | Agent→Quasar | Batch of streamed plugin log lines. | ## Dependencies - [`Magnetar.Protocol/Transport/AgentWireMessage.cs`](AgentWireMessage.cs.md) — `Kind` is set to one of these constants. ## Notes -`PluginLogs` is the newer kind backing the live plugin-log streaming channel (pairs with `AgentWireMessage.PluginLogs`); it replaces stdout capture for the log panel and tolerates Quasar restarts/reconnects. Renaming any value is a breaking protocol change. +`AdminStop` and `AdminRestart` are intentionally separate so Quasar can distinguish "stay off" from "save, exit, and supervisor-relaunch". `PluginLogs` backs the live plugin-log streaming channel (pairs with `AgentWireMessage.PluginLogs`); it replaces stdout capture for the log panel and tolerates Quasar restarts/reconnects. Renaming any value is a breaking protocol change. diff --git a/Docs/Reference/files/Quasar.Agent/AdminPlugin.cs.md b/Docs/Reference/files/Quasar.Agent/AdminPlugin.cs.md index 9ced8b0..27cfaca 100644 --- a/Docs/Reference/files/Quasar.Agent/AdminPlugin.cs.md +++ b/Docs/Reference/files/Quasar.Agent/AdminPlugin.cs.md @@ -3,20 +3,22 @@ **Module:** Quasar.Agent **Kind:** class **Tier:** 1 ## Summary -`AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it reads `AgentOptions`, configures and applies profiler Harmony patches, registers Quasar's admin chat commands (including the root `!stop` override), builds the `GameBridge`, starts a `PluginLogOutbox` (begun before the connection so startup log lines are buffered), wires `StopCommand` to report an admin stop before `!stop` quits the server, and starts an `AgentConnection`. It drives the game-thread snapshot/profiler refresh on each `Update`, refreshes per-character death subscriptions so respawned players are re-hooked, and handles server termination by sending an `AdminStop` signal to Quasar when shutdown was admin-initiated. +`AdminPlugin` is the Magnetar `IPlugin` entry point for the Quasar agent that runs inside the Space Engineers dedicated server. On `Init` it logs Magnetar/Quasar.Agent versions, reads `AgentOptions`, applies profiler Harmony patches, registers Quasar's root admin chat commands (`!stop`, `!restart`, `!quit`), builds the `GameBridge`, starts `PluginLogOutbox`, wires command hooks to early `AdminStop` / `AdminRestart` signals, and starts `AgentConnection`. It drives snapshot/profiler refresh on each `Update`, refreshes death subscriptions, and reports fallback admin shutdown intent when the server terminates outside a Quasar-requested stop. ## Structure **Namespace:** `Quasar.Agent` **Base:** `IPlugin` (VRage.Plugins) **Modifiers:** public, concrete -Fields: `_bridge` (`GameBridge`), `_connection` (`AgentConnection`), `_outbox` (`PluginLogOutbox`), `_adminStopSync`, `_adminStopReported`, `_deathSubscriptionsByIdentityId`, `_recentDeathsByIdentityId`. +Fields: `_bridge` (`GameBridge`), `_connection` (`AgentConnection`), `_outbox` (`PluginLogOutbox`), `_adminStopSync`, `_adminStopReported`, `_adminRestartSync`, `_adminRestartRequested`, `_adminRestartReported`, `_deathSubscriptionsByIdentityId`, `_recentDeathsByIdentityId`. | Member | Description | |---|---| -| `Init(object gameServer)` | Reads `AgentOptions.FromEnvironment()`, calls `AgentProfiler.Configure(options)` and `AgentProfilerPatches.Apply(options)`, registers `StopCommand` through PluginSdk `ServerCommands`, builds `GameBridge`; creates and `Start()`s `PluginLogOutbox` (before the connection loop); constructs `AgentConnection(bridge, WebServiceLocator, options, outbox)`, assigns `StopCommand.AdminStopRequested = ReportAdminStop`, and starts the connection; subscribes `MyVisualScriptLogicProvider.PlayerDied` as a fallback and `ServerControl.Terminating`. | +| `Init(object gameServer)` | Logs startup versions, reads `AgentOptions.FromEnvironment()`, calls `AgentProfiler.Configure(options)` and `AgentProfilerPatches.Apply(options)`, registers `StopCommand`, `RestartCommand`, and `QuitCommand` through PluginSdk `ServerCommands`, builds `GameBridge`; creates and `Start()`s `PluginLogOutbox`; constructs `AgentConnection(bridge, WebServiceLocator, options, outbox)`, assigns command hooks to `ReportAdminStop` / `ReportAdminRestart`, and starts the connection; subscribes `MyVisualScriptLogicProvider.PlayerDied` as a fallback and `ServerControl.Terminating`. | | `Update()` | Delegates to `GameBridge.Update()` each game tick, then periodically scans online human players to hook their current `IMyCharacter.CharacterDied` event. | -| `Dispose()` | Unsubscribes process/session events and character death handlers, clears the `StopCommand` hook, stops the connection, disposes the outbox and bridge, unpatches profiler hooks, nulls references. | -| `OnServerTerminating(ServerTerminationKind kind)` | If `kind == Shutdown` and `!_bridge.QuasarRequestedStop`, calls `ReportAdminStop()` as a fallback for admin/console shutdowns outside the `!stop` command. | -| `ReportAdminStop()` | Locks `_adminStopSync` and sends `AgentConnection.TrySendAdminStop()` until one attempt succeeds, so `!stop` can report early while the termination fallback can retry if the socket was not open yet. | +| `Dispose()` | Unsubscribes process/session events and character death handlers, clears all command hooks, stops the connection, disposes the outbox and bridge, unpatches profiler hooks, nulls references. | +| `OnServerTerminating(ServerTerminationKind kind)` | If `kind == Shutdown`, the bridge was not asked by Quasar to stop, and an admin restart was not already requested, calls `ReportAdminStop()` as a fallback for admin/console shutdowns outside the Quasar command paths. | +| `ReportAdminStop()` | Locks `_adminStopSync` and sends `AgentConnection.TrySendAdminStop()` until one attempt succeeds; suppressed once `!restart` has reported an admin restart. | +| `ReportAdminRestart()` | Marks an admin restart request and sends `AgentConnection.TrySendAdminRestart()` once so Quasar can enter `Restarting` before the process exits. | +| `LogStartupVersions()` | Resolves Magnetar and Quasar.Agent versions and writes them to the game/Magnetar log plus console output. | | `RefreshDeathSubscriptions()` | Once per second, scans `MySession.Static.Players.GetOnlinePlayers()`, skips bots/NPC identities, and hooks each player's current character. | | `HookCharacterDeath(IMyCharacter, long, string)` | Replaces stale per-identity character subscriptions when the character entity changes after respawn. | | `OnCharacterDied(IMyCharacter, long, string)` / `OnPlayerDied(long)` | Resolve the victim name and record a `DeathEventSnapshot` (`DeathType = "Accident"`) via `GameBridge.RecordDeath`; duplicate character/visual-script notifications are suppressed per identity for a short window. | @@ -36,4 +38,4 @@ Fields: `_bridge` (`GameBridge`), `_connection` (`AgentConnection`), `_outbox` ( - `VRage.Plugins` — `IPlugin` ## Notes -The `PluginLogOutbox` is created and started before the `AgentConnection` so plugin log lines emitted during startup are buffered and shipped once connected. Death capture uses the same reliable shape as Torch-style player tracking: hook the live character, then re-hook when respawn gives the player a new character entity. `MyVisualScriptLogicProvider.PlayerDied` remains subscribed only as a fallback. `OnServerTerminating` only acts on `Shutdown`; restarts are left alone so the server can come back. `ReportAdminStop` is shared by `!stop` and the termination fallback so Quasar sees the shutdown intent early while still preserving coverage for other admin-triggered stops; it only suppresses later reports after `TrySendAdminStop()` succeeds. +The `PluginLogOutbox` is created and started before the `AgentConnection` so plugin log lines emitted during startup are buffered and shipped once connected. Death capture hooks the live character and re-hooks after respawn; `MyVisualScriptLogicProvider.PlayerDied` remains subscribed only as a fallback. `ReportAdminStop` is shared by `!stop`, `!quit`, and the termination fallback so Quasar sees shutdown intent early while preserving coverage for other admin-triggered stops. `ReportAdminRestart` gates the termination fallback so `!restart` is not misreported as a stop. diff --git a/Docs/Reference/files/Quasar.Agent/AgentConnection.cs.md b/Docs/Reference/files/Quasar.Agent/AgentConnection.cs.md index 57864a1..96b451b 100644 --- a/Docs/Reference/files/Quasar.Agent/AgentConnection.cs.md +++ b/Docs/Reference/files/Quasar.Agent/AgentConnection.cs.md @@ -3,7 +3,7 @@ **Module:** Quasar.Agent **Kind:** class **Tier:** 1 ## Summary -`AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends a `Hello` handshake plus periodic `Snapshot` messages, streams buffered plugin log batches from a `PluginLogOutbox`, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. +`AgentConnection` manages the WebSocket connection from the in-DS agent to the Quasar supervisor. It runs a reconnect loop on a background task, sends `Hello` plus periodic `Snapshot` messages, streams buffered plugin log batches, receives and dispatches `Command` / `PluginConfigUpdate` / `Ping` messages from Quasar, sends best-effort admin stop/restart signals before command-triggered exits, and performs an autonomous save-and-stop if Quasar stays unreachable past a configurable window. ## Structure **Namespace:** `Quasar.Agent` **Modifiers:** public, concrete @@ -15,6 +15,8 @@ Notable fields: `_bridge`, `_locator`, `_options`, `_outbox` (`PluginLogOutbox`) | `AgentConnection(GameBridge, WebServiceLocator, AgentOptions, PluginLogOutbox)` | Stores dependencies (outbox now injected). | | `Start()` / `Stop()` | Spawn / cancel-and-join (5 s) the background `RunAsync` loop. | | `TrySendAdminStop()` | Best-effort synchronous `AdminStop` send (≤2 s) while the socket is still open; reads `_socket` (volatile) from the game thread and returns whether the signal was sent before timeout/failure. | +| `TrySendAdminRestart()` | Best-effort synchronous `AdminRestart` send before `!restart` exits the process; Quasar uses it to keep goal `On` and enter `Restarting`. | +| `TrySendAdminSignal(kind, label)` | Shared signal helper for admin stop/restart wire messages. | | `RunAsync` (private) | Reconnect loop: locate service, connect (`wss`/`ws`, sub-protocol `quasar.agent.v1`, 20 s keep-alive), send `Hello`, force-send plugin configs, run snapshot + receive loops concurrently. | | `HandleDisconnectedAndDelayAsync` (private) | Tracks outage; once armed (`_hasConnected`) and past the window, calls `ServerControl.SaveAndQuit()`; else waits a jittered delay. | | `ShouldSelfStop` / `NextReconnectDelay` (private) | Offline-window check (`<=0` means stop promptly); jittered reconnect interval (≥1 s). | @@ -35,6 +37,6 @@ Notable fields: `_bridge`, `_locator`, `_options`, `_outbox` (`PluginLogOutbox`) - `Newtonsoft.Json` — camelCase + null-ignore serialization ## Notes -- Sends are serialized via `_sendLock` to prevent concurrent WebSocket writes (snapshot loop, log flush, and `TrySendAdminStop` can all send). `TrySendAdminStop()` returns `false` when the socket is missing, closed, times out, or throws so `AdminPlugin` can leave the termination fallback eligible to retry. +- Sends are serialized via `_sendLock` to prevent concurrent WebSocket writes (snapshot loop, log flush, and admin signal sends can all write). Admin signal methods return `false` when the socket is missing, closed, times out, or throws so `AdminPlugin` can keep fallback reporting eligible where appropriate. - Plugin-log streaming: `FlushPluginLogsAsync` flushes a backlog promptly on reconnect in `MaxBatchLines`-sized chunks; a failed batch is returned to the outbox so no lines are lost. - The autonomous self-stop only arms after at least one successful connection (`_hasConnected`), so a server that never reached Quasar is never auto-stopped. Reconnect uses jitter to spread reconnect storms. diff --git a/Docs/Reference/files/Quasar.Agent/StopCommand.cs.md b/Docs/Reference/files/Quasar.Agent/StopCommand.cs.md index 2a741b6..c91c95f 100644 --- a/Docs/Reference/files/Quasar.Agent/StopCommand.cs.md +++ b/Docs/Reference/files/Quasar.Agent/StopCommand.cs.md @@ -3,22 +3,26 @@ **Module:** Quasar.Agent **Kind:** class **Tier:** 1 ## Summary -`StopCommand` is a Quasar-owned PluginSdk command module for the root `!stop` in-game admin command. It overrides Magnetar's earlier `stop` root by being registered later from `AdminPlugin`, acknowledges the caller, reports an admin stop to Quasar through a static hook wired by `AdminPlugin`, then calls `ServerControl.SaveAndQuit()` on a worker task so the world is saved and the dedicated server process exits. +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. ## Structure -**Namespace:** `Quasar.Agent` **Base:** `CommandModule` (PluginSdk.Commands) **Modifiers:** public, sealed +**Namespace:** `Quasar.Agent` **Base:** `CommandModule` (PluginSdk.Commands) **Modifiers:** public, sealed command classes | Member | Description | |---|---| -| `AdminStopRequested` | Static hook assigned by `AdminPlugin`; lets the command send Quasar's `AdminStop` signal before shutdown starts. | -| `Stop()` | Handles the default root command (`!stop`), responds that save/shutdown has started, then queues admin-stop notification plus `ServerControl.SaveAndQuit()` via `Task.Run`. | -| `TryNotifyAdminStopRequested()` | Best-effort wrapper around `AdminStopRequested` so a notification failure cannot block the server shutdown. | +| `StopCommand.AdminStopRequested` | Static hook assigned by `AdminPlugin`; lets `!stop` send Quasar's `AdminStop` signal before shutdown starts. | +| `StopCommand.Stop()` | Handles bare `!stop`, responds that save/shutdown has started, then queues admin-stop notification plus `ServerControl.SaveAndQuit()` via `Task.Run`. | +| `RestartCommand.AdminRestartRequested` | Static hook assigned by `AdminPlugin`; lets `!restart` send Quasar's `AdminRestart` signal while the socket is still alive. | +| `RestartCommand.Restart()` | Handles bare `!restart`, responds that save/restart has started, then queues admin-restart notification plus `ServerControl.SaveAndQuit()`. | +| `QuitCommand.AdminStopRequested` | Static hook assigned by `AdminPlugin`; lets `!quit` send Quasar's `AdminStop` signal before the process exits. | +| `QuitCommand.Quit()` | Handles bare `!quit`, responds that no-save quit has started, then queues admin-stop notification plus `ServerControl.QuitWithoutSaving()`. | +| `TryNotify*Requested()` | Best-effort wrappers around the static hooks so notification failures cannot block server shutdown. | ## Dependencies -- `PluginSdk` — `ServerControl.SaveAndQuit` +- `PluginSdk` — `ServerControl.SaveAndQuit`, `ServerControl.QuitWithoutSaving` - `PluginSdk.Commands` — `CommandRootAttribute`, `CommandAttribute`, `CommandModule` - `System` — `Action` - `System.Threading.Tasks` — `Task.Run` ## Notes -`[Permission]` is intentionally absent, so PluginSdk applies its fail-safe default of admin-only access. The method uses `[Command("")]` so a bare `!stop` runs the command instead of showing the generated command overview. The command reports `AdminStop` before calling `SaveAndQuit`, flipping the supervisor goal state to `Off` while the agent socket is still available. `AdminPlugin.OnServerTerminating` remains a fallback for other admin-initiated shutdowns and dedupes duplicate reports. +`[Permission]` is intentionally absent, so PluginSdk applies its fail-safe default of admin-only access. Each command uses `[Command("")]` so the bare root (`!stop`, `!restart`, or `!quit`) runs the action instead of showing the generated command overview. `!restart` deliberately avoids Magnetar self-restart and exits after saving so Quasar, not Magnetar, owns the restart state and relaunch. diff --git a/Docs/Reference/files/Quasar/Components/Dashboard/ServerCard.razor.md b/Docs/Reference/files/Quasar/Components/Dashboard/ServerCard.razor.md index 4ad2182..2431c9d 100644 --- a/Docs/Reference/files/Quasar/Components/Dashboard/ServerCard.razor.md +++ b/Docs/Reference/files/Quasar/Components/Dashboard/ServerCard.razor.md @@ -3,12 +3,12 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. +Card component for a single managed server shown on the Dashboard card layout. Displays the server display name, status chip (OFF / STARTING / CONNECTING / OPEN / STOPPING / RESTARTING / CRASHED / FAULTED), host/world caption, last message or health summary, port/direct-connect chip, config-profile chip, management icon buttons (console, clone, template, edit, delete), lifecycle action buttons, and `ServerDetailPanel` body content. The Start button can be disabled by the dashboard while managed runtime prerequisites are still preparing. ## Structure No `@page` route — used as a child component. -**Injection:** `ServerManagementActions ServerActions` for clone/edit/delete/template/console flows. +**Injection:** `ServerManagementActions ServerActions` for clone/edit/delete/template/console flows; `QuasarConfigProfileCatalog` for config-chip labels; `IJSRuntime`, `ISnackbar`, and `NavigationManager` for direct-connect copy feedback. **Parameters:** | Parameter | Type | Notes | @@ -21,6 +21,7 @@ No `@page` route — used as a child component. | `StopRequested` | `EventCallback` | Fires with `UniqueName` when Stop clicked. | | `KillStartingRequested` | `EventCallback` | Fires with `UniqueName` when Kill clicked during `Starting`/`Restarting`. | | `RestartRequested` | `EventCallback` | Fires with `UniqueName` when Restart clicked. | +| `ConfigProfileSelected` | `EventCallback` | Fires with the assigned config-profile id when the config chip is clicked. | **Key MudBlazor components:** `MudCard`, `MudCardHeader`, `MudCardContent`, `MudStack`, `MudChip`, `MudButton`, `MudIconButton`, `MudTooltip`, `MudText`. @@ -28,6 +29,8 @@ No `@page` route — used as a child component. - `ProcessState` — derives `DedicatedServerProcessState` from `Runtime?.State`. - `IsProcessActive`, `CanStart`, `CanStop`, `CanKillStarting`, `CanRestart` — lifecycle button visibility logic. Start is shown only for `Stopped`, `Crashed`, and `Faulted`; Stop is shown only for stable `Running`; Kill is shown for `Starting`/`Restarting`; Restart is shown only for `Running`. No lifecycle button is shown during `Stopping`; Delete is disabled while the process is active. - `CanCreateTemplate` — delegates to `ServerManagementActions.CanCreateWorldTemplate`. +- `CanOpenConfigProfile`, `OpenConfigProfileAsync`, `GetConfigProfileName()` — resolve and invoke the assigned config profile chip. +- `CopyDirectConnectAsync`, `GetDirectConnectAddress`, `ResolveDirectConnectHost` — copy `host:port` for Space Engineers direct connect; wildcard/any-address bindings fall back to the browser host and IPv6 hosts are bracketed. - `OpenConsoleAsync`, `CloneAsync`, `CreateTemplateAsync`, `EditAsync`, `DeleteAsync` — delegate to `ServerManagementActions`. - `GetDisplayName()` — prefers `Server.DisplayName`, falls back to `Agent.ServerDisplayName`, then `UniqueName`. - `GetHostLabel()` — shows `Agent.HostDisplayName` or "Local host". @@ -37,6 +40,7 @@ No `@page` route — used as a child component. ## Dependencies - [`Quasar/Components/Dashboard/ServerDetailPanel.razor`](ServerDetailPanel.razor.md) — embedded in card body - [`Quasar/Services/ServerManagementActions.cs`](../../Services/ServerManagementActions.cs.md) — clone/edit/delete/template/console dialog flows +- `Quasar/Services/QuasarConfigProfileCatalog.cs` — config profile lookup - `Magnetar.Protocol.Model.DedicatedServerDefinition` — static server config type - `Magnetar.Protocol.Model.DedicatedServerRuntimeSnapshot` — runtime state parameter - `Magnetar.Protocol.Model.DedicatedServerProcessState` — process state enum diff --git a/Docs/Reference/files/Quasar/Components/Pages/Home.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Home.razor.md index 0597b5e..2035b70 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Home.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Home.razor.md @@ -3,11 +3,11 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -Routable dashboard and primary server control surface at `/`. Shows a top-of-dashboard data handling consent prompt until YES/NO is stored, a five-step first-run setup wizard that auto-opens only while no dedicated server has been created yet, a Managed Runtime panel with SteamCMD, Magnetar, and Dedicated Server readiness/download/update progress, summary KPI cards (online servers, players online, health warnings, errors), an optional problem banner, and a switchable server section. The default card view renders `ServerCard`s for live operations; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated, so the landing page does not build hidden list/table DOM. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. The wizard opens full-screen page dialogs for config-template, world-template, and server creation. +Routable dashboard and primary server control surface at `/`. Shows data-handling consent, the first-run setup wizard, managed-runtime readiness/update progress, KPI cards, a problem banner, and a switchable server section whose Cards/List selection persists in browser local storage. The default card view renders `ServerCard`s in the same unique-name catalog order as the list, includes a Create Server button that opens the server editor immediately through `ServersPageDialog`, and passes config-profile click callbacks to cards; `?view=list` embeds the `Servers` table without MudBlazor's responsive card layout. Only the selected server view is generated. Servers can be started, stopped, restarted, and opened in the log dialog directly from the dashboard; Stop and Kill actions require confirmation. ## Structure - **Route:** `@page "/"`; **Implements:** `IDisposable` -- **`[Inject]`:** `AgentRegistry Registry`, `DedicatedServerCatalog ServerCatalog`, `DedicatedServerSupervisor Supervisor`, `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarWorldTemplateCatalog WorldTemplates`, `ManagedRuntimeWarmupService RuntimeWarmup`, `DataHandlingConsentCatalog DataHandlingConsent`, `IDialogService DialogService`, `ISnackbar Snackbar`, `NavigationManager Navigation` +- **`[Inject]`:** `AgentRegistry Registry`, `DedicatedServerCatalog ServerCatalog`, `DedicatedServerSupervisor Supervisor`, `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarWorldTemplateCatalog WorldTemplates`, `ManagedRuntimeWarmupService RuntimeWarmup`, `DataHandlingConsentCatalog DataHandlingConsent`, `IDialogService DialogService`, `ISnackbar Snackbar`, `NavigationManager Navigation`, `ILocalStorageService LocalStorage` - **`[SupplyParameterFromQuery(Name = "view")]`:** `ServerViewQuery` — `list` selects the embedded server-management table; any other value keeps the default card layout. - **Key UI sections** - Data Handling Consent prompt — top `MudPaper` shown only while `DataHandlingConsent.GetSettings().ConsentGranted` is `null`; asks YES/NO with equal outlined buttons and saves through `DataHandlingConsent.SaveAsync`. It explains that no stored decision means Magnetar starts with no consent. @@ -23,14 +23,14 @@ Routable dashboard and primary server control surface at `/`. Shows a top-of-das - Problem banner (`MudAlert`) — first crashed/faulted server with a `Clear Error Status` action, else first unhealthy server, else first warning instance message. - Managed Runtime panel — visible while warmup is pending/running/failed or any runtime component is actively checking/downloading/installing. It shows overall warmup/update state plus SteamCMD, Magnetar, and Dedicated Server component rows with status text, copyable diagnostic paths, and determinate/indeterminate `MudProgressLinear` progress. This keeps the panel visible for hourly Magnetar update checks even after initial readiness has completed. Start buttons are disabled until startup readiness is complete. A Retry button appears on failed Dedicated Server or Magnetar rows. - KPI summary grid (4 cards): Online Servers, Players Online, Health Warnings (warning tint when > 0), Errors (error tint when > 0). - - Server view toolbar — title plus Cards/List buttons. `SetServerView` updates the URL to `/` or `/?view=list` with replace navigation. - - Card view — one `` per server with runtime snapshot, connected agent, and `LaunchBlocked` readiness state; callbacks `StartRequested`/`StopRequested`/`KillStartingRequested`/`RestartRequested`. Cards provide their own clone/delete/edit/template/console buttons through `ServerManagementActions`. When no servers and wizard hidden, shows an info alert. + - Server view toolbar — title plus Create Server (card view only) and Cards/List buttons. `SetServerViewAsync` updates the URL to `/` or `/?view=list`, and the selection is stored under `quasar.dashboard.serverView`. + - Card view — one `` per server with runtime snapshot, connected agent, and `LaunchBlocked` readiness state; callbacks `StartRequested`/`StopRequested`/`KillStartingRequested`/`RestartRequested`/`ConfigProfileSelected`. Cards provide their own clone/delete/edit/template/console buttons through `ServerManagementActions`. When no servers and wizard hidden, shows an info alert. - List view — embeds `` so the server-management table appears under the dashboard status cards and stays a true table with horizontal scrolling instead of auto-switching to MudBlazor's narrow-card presentation. Start buttons inherit the dashboard runtime-warmup block message. - **Setup-wizard state (fields):** `_setupWizardRequested`, `_setupWizardDismissed`, `_setupWizardActive`, `_skippedSetupSteps` (HashSet), `_setupStepOverride`. - **Wizard visibility:** `ShowSetupWizard => (_setupWizardActive || _setupWizardRequested) && !_setupWizardDismissed`. `OnInitialized` sets `_setupWizardActive = ConfiguredServerCount == 0`, so the wizard auto-opens only until the first server exists; once shown it stays for the session. `ShowSetupWizardAgain` re-requests it; `HideSetupWizard` dismisses it. - **Step model:** `IsSetupStepComplete(0..4)` keyed on config-profile count / world-template count (or skipped) / server count / running count / connected-agent count; `CurrentSetupStep` returns the first incomplete step (or a clamped override); `SetupProgressPercent`, `SkipCurrentSetupStep`, `GoToPreviousSetupStep`. - **KPI/state computed props:** `OnlineServerCount`, `PlayersOnline`, `ConfiguredServerCount`, `RunningServerCount`, `WarningServerCount`, `UnhealthyServerCount`, `ConnectedAgentCount`, `ProblemBanner` (severity/message/clearable unique name), `StartableServers`, `LaunchedServers`, `IsLaunchBlocked`, `ShowDataHandlingConsentPrompt`, `ServerView`, `IsCardsView`, `IsListView`. -- **Actions:** `SaveDataHandlingConsentAsync` persists the YES/NO choice and reports success/failure; `StartAsync` blocks with a warning snackbar while managed runtime warmup is incomplete, otherwise sets goal On and explicitly starts via `Supervisor.StartServerAsync` so `Crashed`/`Faulted` can be operator-retried; `RetryRuntimeWarmupAsync` calls `RuntimeWarmup.RetryAsync` from failed Dedicated Server or Magnetar runtime rows; `StopAsync` confirms then sets goal Off; `ClearErrorStatusAsync` calls `Supervisor.ClearErrorStatusAsync` to acknowledge a crashed/faulted server and clear it to stopped/goal Off; `KillStartingAsync` confirms then calls `Supervisor.KillStartingServerAsync`; `RestartAsync` calls `Supervisor.RestartServerAsync`; `SetServerView` switches card/list URL state; `OpenConfigProfileFromServerListAsync` opens `ConfigsPageDialog` with `InitialProfileId` when a config link is clicked from the embedded list. `ShowFullScreenPageDialogAsync` opens full-screen `MudDialog`s and accepts optional `DialogParameters`. +- **Actions:** `SaveDataHandlingConsentAsync` persists the YES/NO choice and reports success/failure; `StartAsync` blocks with a warning snackbar while managed runtime warmup is incomplete, otherwise sets goal On and explicitly starts via `Supervisor.StartServerAsync` so `Crashed`/`Faulted` can be operator-retried; `RetryRuntimeWarmupAsync` calls `RuntimeWarmup.RetryAsync` from failed Dedicated Server or Magnetar runtime rows; `StopAsync` confirms then sets goal Off; `ClearErrorStatusAsync` calls `Supervisor.ClearErrorStatusAsync`; `KillStartingAsync` confirms then calls `Supervisor.KillStartingServerAsync`; `RestartAsync` calls `Supervisor.RestartServerAsync`; `SetServerViewAsync` switches card/list URL state and persists preference; `OpenConfigProfileFromServerListAsync` opens `ConfigsPageDialog` with `InitialProfileId` when a config link is clicked from list or card view. `ShowFullScreenPageDialogAsync` opens full-screen `MudDialog`s and accepts optional `DialogParameters`. - **Helpers:** `GetRuntime`/`GetAgent`/`IsRunning`/`IsOpen`, `GetServerSetupSummary`, `GetSetupRuntimeSummary`, `GetSetup*Text`/`GetSetup*Color`, `GetRuntimeWarmup*`, `GetRuntimeComponent*`, `CanRetryRuntimeComponent`, `GetProblemCardClass`, `GetNextSetupHint`, `GetSetupProgressText`. - **Event subscriptions** (subscribed in `OnInitialized`, released in `Dispose`): `Registry.Changed`, `ServerCatalog.Changed`, `Supervisor.Changed`, `ConfigProfiles.Changed`, `WorldTemplates.Changed`, `RuntimeWarmup.Changed`, `DataHandlingConsent.Changed`; `HandleRegistryChanged` marshals `StateHasChanged`. @@ -43,12 +43,12 @@ Routable dashboard and primary server control surface at `/`. Shows a top-of-das - [`Quasar/Services/ManagedRuntimeWarmupService.cs`](../../Services/ManagedRuntimeWarmupService.cs.md) - [`Quasar/Services/WebServiceOptions.cs`](../../Services/WebServiceOptions.cs.md) — `DataHandlingConsentCatalog` - [`Quasar/Components/Shared/CopyablePath.razor`](../Shared/CopyablePath.razor.md) -- `Quasar/Components/ServerCard.razor` (card-view child component) +- [`Quasar/Components/Dashboard/ServerCard.razor`](../Dashboard/ServerCard.razor.md) (card-view child component) - [`Quasar/Components/Pages/Servers.razor`](Servers.razor.md) (embedded list view) - `Quasar/Components/Pages/ConfigsPageDialog.razor`, `WorldTemplatesPageDialog.razor`, `ServersPageDialog.razor` (full-screen wizard dialogs) - `Magnetar.Protocol` — process/health/goal state enums, runtime snapshots - MudBlazor (`MudProgressLinear`, `MudAlert`, `MudGrid`, `MudChip`, `MudPaper`, `IDialogService`, `ISnackbar`) ## Notes -- Server existence (`ConfiguredServerCount`) is the authoritative, persisted signal for auto-opening the wizard: once the first server is created the wizard never auto-opens again, but it can be reopened with "Restart Setup Wizard". There is no browser local-storage persistence of wizard state. +- Server existence (`ConfiguredServerCount`) is the authoritative, persisted signal for auto-opening the wizard: once the first server is created the wizard never auto-opens again, but it can be reopened with "Restart Setup Wizard". Dashboard card/list view state is browser-local; wizard state is not. - The wizard is reactive: as servers start and agents attach, the steps advance automatically via the `Changed` subscriptions. diff --git a/Docs/Reference/files/Quasar/Components/Pages/Servers.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Servers.razor.md index 8968fdd..e38149f 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Servers.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Servers.razor.md @@ -3,19 +3,20 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel` with the server definition, runtime snapshot, and live agent data. A separate panel lists "unmanaged" agents that report in without a matching server definition. +Embeddable server-management component used by the dashboard list view and full-screen server dialog. Renders a sortable table of `DedicatedServerDefinition`s with live process/agent status, clickable config-profile names, copyable direct-connect ports, and a rightmost action column for lifecycle, console, clone, template, edit, and delete commands; destructive Stop/Kill/Delete actions require confirmation. Clone asks whether to copy world state or leave the clone without a world, always clears path overrides so the clone gets independent DS/config/world paths, and refuses explicit path reuse. Expanded rows embed a `ServerDetailPanel`; a separate panel lists unmanaged agents. ## Structure - **No `@page` route**; **`@implements IDisposable`**. `Home.razor` embeds this component for the dashboard list view selected by `?view=list`. -- **`[Inject]`:** `DedicatedServerCatalog ServerCatalog`, `DedicatedServerSupervisor Supervisor`, `AgentRegistry Registry`, `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarWorldTemplateCatalog WorldTemplates`, `IDialogService DialogService`, `WebServiceOptions Options`, `ISnackbar Snackbar`, `NavigationManager Navigation` +- **`[Inject]`:** `DedicatedServerCatalog ServerCatalog`, `DedicatedServerSupervisor Supervisor`, `AgentRegistry Registry`, `QuasarConfigProfileCatalog ConfigProfiles`, `QuasarWorldTemplateCatalog WorldTemplates`, `IDialogService DialogService`, `WebServiceOptions Options`, `ISnackbar Snackbar`, `NavigationManager Navigation`, `IJSRuntime JS` - **`[Parameter]`:** `EventCallback ConfigProfileSelected` — when set, clicking a config name invokes this instead of navigating to `/configs`. +- **`[Parameter]`:** `OpenCreateOnStart` — after first render, opens the Create Server editor once; used by `ServersPageDialog` when dashboard card view wants the create button to behave like list mode. - **`[Parameter]`:** `Embedded` — suppresses the page title when the component is hosted inside another page. - **`[Parameter]`:** `HideHeader` — suppresses the top title/description copy for embedded use. - **`[Parameter]`:** `DisableResponsiveLayout` — sets the `MudTable` breakpoint to `Breakpoint.None`, keeping the table layout instead of MudBlazor's responsive card/list rendering. - **`[Parameter]`:** `LaunchBlocked`, `LaunchBlockedMessage` — disables/guards Start buttons while the managed runtime is still preparing and shows the supplied warning message. - **Key UI** - Server count chip + "Create Server" button. - - `MudTable` with `Class="servers-list-table"`, leading expand toggle, sortable Status / Unique name / Port / Config / Players / Process / Agent / Name columns, and a rightmost unlabeled action column containing Start / Stop / Kill / Restart according to process state plus Console, Clone, Template, Edit, and Delete. + - `MudTable` with `Class="servers-list-table"`, leading expand toggle, sortable Status / Unique name / Port / Config / Players / Process / Agent / Name columns, and a rightmost unlabeled action column containing Start / Stop / Kill / Restart according to process state plus Console, Clone, Template, Edit, and Delete. The Port cell is a copy action for direct-connect `host:port`; wildcard/any-address bindings fall back to the browser host. - `ChildRowContent` renders `` for expanded rows inside a `servers-list-detail-row`, which opts the detail area out of the global table-row hover inversion while allowing nested tables to keep their own row hover behavior. - Unmanaged Agents `MudPaper` (when any): one `MudExpansionPanel` per connected agent lacking a definition, each containing a `ServerDetailPanel`. - **State/data:** `_expanded` HashSet drives row expansion; `ServerDefinitions`, `RuntimeSnapshots` (by unique name), `AgentsByUniqueName` (connected, newest by `LastSeenUtc`), `UnmanagedAgents`. @@ -26,7 +27,7 @@ Embeddable server-management component used by the dashboard list view and full- - `CreateWorldTemplateAsync` — validates the server is stopped and its world path has `Sandbox.sbc`, opens `WorldTemplateFromServerDialog`, then `WorldTemplates.ImportAsync`. - `OpenConfigProfileAsync` — invokes `ConfigProfileSelected` or navigates to `/configs?profileId=...`. - **Lifecycle controls:** `StartAsync` first respects `LaunchBlocked`, then sets goal On and explicitly starts so `Crashed`/`Faulted` can be operator-retried; `StopAsync` confirms then sets goal Off; `KillStartingAsync` confirms then calls `Supervisor.KillStartingServerAsync`; `RestartAsync` calls `Supervisor.RestartServerAsync`; `DeleteAsync` confirms (servers must be stopped), then `ServerCatalog.DeleteAsync` + `Registry.PruneDisconnectedByUniqueName`. `Starting` shows Kill only; `Restarting` shows Kill; `Running` shows Stop and Restart; `Stopped`/`Crashed`/`Faulted` show Start. -- **Helpers:** `GetDisplayName`, `GetPlayerCount` (uses configured `MaxPlayers` when available), `GetProcessDisplay`, `GetConfigProfileName`/`CanOpenConfigProfile`, `GetStateText`/`GetStateColor`, `IsRunning`, `CanStartServer`, `CanStopServer`, `CanKillStartingServer`, `CanRestartServer`, `CanCreateWorldTemplate`, `GetAttachmentStatus`, plus sort-value helpers. +- **Helpers:** `GetDisplayName`, `GetPlayerCount` (uses configured `MaxPlayers` when available), `GetProcessDisplay`, `GetConfigProfileName`/`CanOpenConfigProfile`, `CopyDirectConnectAsync`/`GetDirectConnectAddress`, `GetStateText`/`GetStateColor`, `IsRunning`, `CanStartServer`, `CanStopServer`, `CanKillStartingServer`, `CanRestartServer`, `CanCreateWorldTemplate`, `GetAttachmentStatus`, plus sort-value helpers. - **Blank/clone factories/helpers:** `CreateBlank` seeds defaults (port via `AllocateNextPort` from 27016, `ServerIP` 0.0.0.0, health/restart policy fields, health monitoring driven by `Options.DisableServerHealthMonitoring`); `MakeCopyIdentifier` generates a unique `-copy` name; clone helpers choose world mode, guard independent paths, find latest SE `Backup/` snapshots, copy directories, and compare normalized paths; `NormalizeWhitespace`. - Subscribes to `ServerCatalog`, `Supervisor`, `Registry`, `ConfigProfiles`, `WorldTemplates` `.Changed` in `OnInitialized`, releases in `Dispose`; `HandleChanged` marshals `StateHasChanged`. diff --git a/Docs/Reference/files/Quasar/Components/Pages/ServersPageDialog.razor.md b/Docs/Reference/files/Quasar/Components/Pages/ServersPageDialog.razor.md index 942064b..7bd0f39 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/ServersPageDialog.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/ServersPageDialog.razor.md @@ -3,16 +3,17 @@ **Module:** Quasar.Components **Kind:** Blazor component **Tier:** 2 ## Summary -Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. +Thin MudBlazor full-screen dialog wrapper around the `` page component, used from dashboard-style views that want to open server management without navigating away. It can optionally ask the embedded Servers component to open the Create Server editor immediately. When the user clicks a config profile link inside the embedded Servers view, the dialog closes itself and opens `ConfigsPageDialog` in its place. ## Structure - **No `@page` route** — dialog only. - **`[Inject]`** - `IDialogService DialogService` - **`[CascadingParameter]` `IMudDialogInstance MudDialog`** +- **`[Parameter]` `OpenCreateOnStart`** — passed through to `` so dashboard/create flows can open the server editor immediately after the dialog renders. - **Key UI** - `MudDialog` with back arrow in `TitleContent`. - - `` — the full Servers page embedded in the dialog body. + - `` — the full Servers page embedded in the dialog body. - "Done" close button. - **`OpenConfigProfileAsync(string configProfileId)`** — closes this dialog, then opens `ConfigsPageDialog` full-screen with `InitialProfileId` set. diff --git a/Docs/Reference/files/Quasar/Components/Pages/Updates.razor.md b/Docs/Reference/files/Quasar/Components/Pages/Updates.razor.md index e84dd4a..964aaae 100644 --- a/Docs/Reference/files/Quasar/Components/Pages/Updates.razor.md +++ b/Docs/Reference/files/Quasar/Components/Pages/Updates.razor.md @@ -4,7 +4,7 @@ ## Summary -Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases. It shows current update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual check/stage/activate actions for the selected UI worker version, can force a detected Bootstrap launcher update to activate immediately when running under Bootstrap, displays configured GitHub release source and asset names, provides controls for including prerelease versions plus choosing automatic or manual UI staging, and renders a git-style `appsettings.json` conflict editor with a copyable conflict-file path when staging cannot auto-merge settings. +Routable MudBlazor page at `/settings/updates` for checking, staging, activating, and rolling back Quasar UI worker releases plus inspecting/updating managed runtime components. It shows current Quasar update status from `QuasarUpdateService`, separates selectable Quasar UI releases from launcher candidates, exposes manual Quasar check/stage/activate actions, can force a detected Bootstrap launcher update, displays installed Magnetar and Space Engineers Dedicated Server versions/paths at all times, exposes separate manual Magnetar and DS checks, provides prerelease/auto-staging controls, and renders a git-style `appsettings.json` conflict editor when staging cannot auto-merge settings. ## Structure @@ -16,6 +16,7 @@ Authorization: `QuasarPolicyNames.CanManageSecurity` - `QuasarUpdateService` — snapshot source and action API - `QuasarUpdateOptions` — configured GitHub owner/repository/assets/check interval - `WebServiceOptions` — current UI and Bootstrap versions +- `ManagedRuntimeWarmupService` — managed Magnetar/DS snapshot plus manual update checks - `ISnackbar` — user feedback for update actions - `IDialogService` — confirmation dialogs before enabling prerelease updates or forcing Bootstrap activation - `IJSRuntime` — starts browser-side health polling before a forced Bootstrap restart drops the circuit @@ -24,8 +25,10 @@ Authorization: `QuasarPolicyNames.CanManageSecurity` | Member | Description | |---|---| -| `OnInitialized()` / `Dispose()` | Subscribes/unsubscribes to `UpdateService.Changed` and initializes `_snapshot`. | -| `CheckNowAsync()` | Runs an immediate release check through `QuasarUpdateService.CheckNowAsync()`. | +| `OnInitialized()` / `Dispose()` | Subscribes/unsubscribes to `UpdateService.Changed` and `RuntimeWarmup.Changed`; initializes Quasar and managed-runtime snapshots. | +| `CheckNowAsync()` | Runs an immediate Quasar release check through `QuasarUpdateService.CheckNowAsync()`. | +| `CheckMagnetarNowAsync()` | Runs an immediate managed Magnetar check through `ManagedRuntimeWarmupService.CheckMagnetarNowAsync()`. | +| `CheckDedicatedServerNowAsync()` | Runs an immediate managed DS check through `ManagedRuntimeWarmupService.CheckDedicatedServerNowAsync()`. | | `HandleSelectedWebVersionChanged(...)` | Selects the UI release to stage/install from the discovered list, including older rollback targets. | | `StageAsync()` | Downloads and stages the selected Quasar UI release unless it is already current or staged; if appsettings rollover conflicts, loads the conflict text and warns instead of reporting success. | | `ActivateAsync()` | Requests staged UI activation; the update service promotes the staged payload into the managed active-release directory and writes the active-release pointer. Older staged releases are allowed for rollback. | @@ -33,8 +36,10 @@ Authorization: `QuasarPolicyNames.CanManageSecurity` | `HandleIncludePrereleaseChanged(bool)` | Confirms before enabling prerelease updates, persists the stream setting through `QuasarUpdateService`, refreshes the release list, and shows a strong warning while prereleases are enabled. | | `HandleAutoStageWebUpdatesChanged(bool)` | Persists whether release checks should automatically download/stage a newer UI release or only queue releases for manual staging. | | `LoadAppSettingsConflictAsync()` / `SaveAppSettingsResolutionAsync()` / `ForceReleaseAppSettingsAsync()` | Reads the staged conflict file, saves a manually resolved JSON file, or force-restages release defaults after confirmation. | -| `RunBusyAsync(...)` | Shared busy-state/error/snackbar wrapper for the three actions. | +| `RunBusyAsync(...)` | Shared busy-state/error/snackbar wrapper for Quasar update actions. | +| `RunRuntimeBusyAsync(...)` | Shared busy-state/error/snackbar wrapper for managed-runtime checks. | | `GetStatusSeverity()` | Maps `QuasarUpdateStatus` to MudBlazor alert severity. | +| `ManagedRuntimeRows` / `FormatRuntimeVersion(...)` | Render installed managed-runtime version/path/status rows for Magnetar and Dedicated Server. | | `FormatBootstrapVersion()` | Shows the Bootstrap launcher version when the worker was started by Bootstrap, otherwise reports that Bootstrap is not managing this worker. | | `FormatWebReleaseOption(...)` / `FormatWebReleaseStatus(...)` | Labels selectable UI releases as current, newer, rollback, prerelease, and/or staged. | @@ -45,3 +50,4 @@ Authorization: `QuasarPolicyNames.CanManageSecurity` - [`Quasar/Services/Updates/QuasarUpdateSnapshot.cs`](../../Services/Updates/QuasarUpdateSnapshot.cs.md) — status/candidate DTOs displayed by the page - [`Quasar/Components/Shared/CopyablePath.razor`](../Shared/CopyablePath.razor.md) - `Quasar/Services/WebServiceOptions.cs` — current Quasar UI and Bootstrap versions plus launcher-managed detection +- [`Quasar/Services/ManagedRuntimeWarmupService.cs`](../../Services/ManagedRuntimeWarmupService.cs.md) — managed-runtime versions and manual checks diff --git a/Docs/Reference/files/Quasar/Program.cs.md b/Docs/Reference/files/Quasar/Program.cs.md index 57aa68e..f0979bf 100644 --- a/Docs/Reference/files/Quasar/Program.cs.md +++ b/Docs/Reference/files/Quasar/Program.cs.md @@ -3,7 +3,7 @@ **Module:** Quasar.Host **Kind:** class **Tier:** 1 ## Summary -The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. +The ASP.NET Core / Blazor Server entry point for the Quasar supervisor host. `Program.Main` builds the `WebApplication`, registers every DI service, configures authentication and authorization, wires the middleware pipeline, maps HTTP/WebSocket endpoints, logs Quasar startup version/host/data-directory details, and runs the app. It is the system wiring hub — essentially every service in the process is registered here. ## Structure Namespace `Quasar`; `public class Program` with `static void Main(string[] args)`. @@ -31,6 +31,8 @@ Namespace `Quasar`; `public class Program` with `static void Main(string[] args) Pipeline: exception handler (prod) → forwarded headers (`X-Forwarded-For` / proto / host from loopback or configured `TrustedProxies`) → status-code re-execute (`/not-found`) → `UseWebSockets` (30 s keep-alive) → `UseAuthentication` → inline trusted-network principal injection → `UseAuthorization` → `UseAntiforgery`. Endpoints: `GET /api/health` (status/worker/host/version/baseUrl/connectedAgents/configuredServers/runningServers), `GET /api/discovery` (manifest), `GET /api/analytics/series` (browser-fetched chart series), `GET /api/servers/{uniqueName}/logs/server/download` (streams the newest `SpaceEngineersDedicated*.log` for a configured server), `GET /api/servers/{uniqueName}/logs/magnetar/download` (streams that server's Magnetar `info.log`), `GET /api/discord/log/download` (streams the dedicated Discord integration log) — server log downloads require `CanView` and the Discord log download requires `CanManageDiscord` when auth is enabled, `GET /login` (Steam challenge or unavailable page), `GET /logout`, `GET /access-denied` (standalone branded 403 page for authenticated users lacking a Quasar role), `POST /api/internal/drain` (launcher-token + trusted-network gated; `delaySeconds`/`stopServers` params), `GET /api/backup/download` (`QuasarBackupService.CreateBackup` -> streams a fresh ZIP), `GET /api/backup/download/{name}` (downloads an existing stored backup by name from the configured backup directory) — both `RequireAuthorization(CanManageSecurity)` when auth enabled, `Map /ws/agent` -> `AgentSocketHandler`, `MapStaticAssets()`, `/branding` physical static files served from the persistent Quasar data directory, `MapRazorComponents()` (interactive server; `RequireAuthorization(CanView)` when auth enabled). +Startup log: after the app is built and before `Run()`, the host logger writes the Quasar worker version, Bootstrap version (or `none`), host id, and data directory to the Quasar log file. + ### POSIX signals + helpers On Linux/macOS, SIGINT/SIGTERM handlers either `StopApplication` (when preserving managed servers) or `QuasarShutdownService.ShutdownAsync`. Helpers: `CreateForwardedHeadersOptions`, `AddTrustedProxy`, `DownloadLogFile`, `ResolveLatestDedicatedServerLogPath`, `ResolveMagnetarInfoLogPath`, `CompositeDisposable`, `EmptyDisposable`, `SanitizeReturnUrl`, `ExtractSteamId`, `AddOrReplaceClaim`, `ShouldUseSourceStaticWebAssets`, `ShouldListenOnAnyInterface`. diff --git a/Docs/Reference/files/Quasar/Services/AgentSocketHandler.cs.md b/Docs/Reference/files/Quasar/Services/AgentSocketHandler.cs.md index 9d950db..4ca2ad6 100644 --- a/Docs/Reference/files/Quasar/Services/AgentSocketHandler.cs.md +++ b/Docs/Reference/files/Quasar/Services/AgentSocketHandler.cs.md @@ -4,7 +4,7 @@ ## Summary -`AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, and marks the connection disconnected in the registry on teardown. It is the bridge between the raw WebSocket transport and the rest of the supervisor stack. +`AgentSocketHandler` is the HTTP/WebSocket entry point for incoming Quasar.Agent connections. It accepts the `quasar.agent.v1` sub-protocol, drives the per-connection read loop, dispatches each `AgentWireMessage` to the appropriate service, handles agent-originated admin stop/restart lifecycle signals, and marks the connection disconnected in the registry on teardown. ## Structure @@ -15,7 +15,7 @@ Namespace: `Quasar.Services` | Member | Description | |---|---| | `HandleAsync(HttpContext)` | Rejects non-WebSocket requests (400); upgrades to WebSocket, assigns a GUID connection id, runs the read loop, calls `_registry.MarkDisconnected` + closes the socket in `finally`. Loop token is a linked CTS of `RequestAborted` + `_lifetime.ApplicationStopping` so an in-flight `ReceiveAsync` cannot stall graceful shutdown ~30s. | -| `ProcessMessageAsync(message, connectionId, socket, ct)` | Dispatches on `WireMessageKind`: `Hello` → `UpsertHello` (wires a per-socket send callback); `Snapshot` → `UpdateSnapshot`; `CommandResult` → `UpdateCommandResult`; `PluginConfigSnapshot` → `_pluginConfigService.IngestSnapshot`; `PluginLogs` → `IngestPluginLogs`; `AdminStop` → `_supervisor.SetGoalStateAsync(Off)` using `_lifetime.ApplicationStopping`; `Ping` → `Pong` reply; default → debug-log and ignore. | +| `ProcessMessageAsync(message, connectionId, socket, ct)` | Dispatches on `WireMessageKind`: `Hello` → `UpsertHello` (wires a per-socket send callback); `Snapshot` → `UpdateSnapshot`; `CommandResult` → `UpdateCommandResult`; `PluginConfigSnapshot` → `_pluginConfigService.IngestSnapshot`; `PluginLogs` → `IngestPluginLogs`; `AdminStop` → `_supervisor.SetGoalStateAsync(Off)` using `_lifetime.ApplicationStopping`; `AdminRestart` → `_supervisor.BeginAdminRestartAsync(...)`; `Ping` → `Pong` reply; default → debug-log and ignore. | | `IngestPluginLogs(PluginLogBatch, connectionId)` | Resolves unique name from the connection's Hello; parses each line with `PluginLogStream.TryParseSinkLine` and appends to the live buffer. | | `ReceiveAsync(WebSocket, ct)` | Reads fragmented text frames (16 KB buffer) into a `MemoryStream`; deserialises as `AgentWireMessage`. | | `SendAsync(WebSocket, AgentWireMessage, ct)` | Serialises and sends a single text frame. | @@ -25,7 +25,7 @@ JSON options: `JsonSerializerDefaults.Web`, `WhenWritingNull`. ## Dependencies - `Quasar/Services/AgentRegistry.cs` — target of `Hello`/`Snapshot`/`CommandResult`, unique-name resolution, disconnect marking -- [`Quasar/Services/DedicatedServerSupervisor.cs`](DedicatedServerSupervisor.cs.md) — `SetGoalStateAsync` on `AdminStop` +- [`Quasar/Services/DedicatedServerSupervisor.cs`](DedicatedServerSupervisor.cs.md) — `SetGoalStateAsync` on `AdminStop`, `BeginAdminRestartAsync` on `AdminRestart` - `Quasar/Services/PluginSdk/PluginConfigService.cs` — `IngestSnapshot` - [`Quasar/Services/PluginSdk/PluginLogStream.cs`](PluginSdk/PluginLogStream.cs.md) — `TryParseSinkLine` / `Append` for `PluginLogs` - `Quasar/Models/` — `DedicatedServerGoalState` @@ -34,4 +34,4 @@ JSON options: `JsonSerializerDefaults.Web`, `WhenWritingNull`. ## Notes -Critical cancellation design: read/reply paths use `context.RequestAborted`, but persistent state mutations (`AdminStop` goal-state write) use `_lifetime.ApplicationStopping`. The agent closes its socket the instant it sends a signal (its process is exiting); using the request token would cancel the write mid-flight and let the exit be misread as a crash and restarted. Plugin log lines arrive over this agent channel (not the supervisor's stdout pump) precisely so they keep flowing after Quasar restarts and reconnects to a detached, still-running server daemon. +Critical cancellation design: read/reply paths use `context.RequestAborted`, but persistent lifecycle mutations (`AdminStop` and `AdminRestart`) use `_lifetime.ApplicationStopping`. The agent closes its socket soon after sending a signal (its process is exiting); using the request token would cancel the write mid-flight and let the exit be misread. Plugin log lines arrive over this agent channel (not the supervisor's stdout pump) precisely so they keep flowing after Quasar restarts and reconnects to a detached, still-running server daemon. diff --git a/Docs/Reference/files/Quasar/Services/DedicatedServerSupervisor.cs.md b/Docs/Reference/files/Quasar/Services/DedicatedServerSupervisor.cs.md index 49d974a..7149889 100644 --- a/Docs/Reference/files/Quasar/Services/DedicatedServerSupervisor.cs.md +++ b/Docs/Reference/files/Quasar/Services/DedicatedServerSupervisor.cs.md @@ -4,7 +4,7 @@ ## Summary -`DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, and coordinates graceful stop (save + stop commands to the agent before kill) plus scheduled and maximum-uptime restarts. +`DedicatedServerSupervisor` is the heart of Quasar's process management. It is an `IHostedService` that maintains in-memory `ManagedServerState` for every configured dedicated server, runs a 2-second reconcile loop that starts/stops/restarts processes to match goal state, evaluates server health (agent heartbeat, simulation frame progress, uptime thresholds), rotates and prunes Quasar-captured DS stdout/stderr logs, captures mod-download failure lines for dashboard surfacing, persists runtime state across Quasar worker restarts and **adopts surviving detached processes by PID on startup**, carries per-server advertised server/world names into launch preparation, warns when the bundled Quasar.Agent DLL hash differs from a running server's deployed Magnetar local DLL, coordinates graceful stop (save + stop commands to the agent before kill), and tracks admin-requested `!restart` as a supervisor-owned restart instead of a crash/reconnect wait. ## Structure @@ -24,13 +24,14 @@ Namespace: `Quasar.Services` | `StopServerAsync(uniqueName, forceAfter?, ct)` | Cancels in-flight launch prep, sends `SaveWorld` + `StopServer` to the agent when attached, waits for exit, kills the process tree if the grace window expires. The default grace window is 30 seconds. | | `KillStartingServerAsync(...)` | Immediate launch cancel/kill path for `Starting`/`Restarting`: flips goal Off, cancels launch prep if no process handle exists yet, or kills the starting process tree immediately when it does. | | `RestartServerAsync(...)` | Sets goal On + AutoStart, logs deployed-vs-bundled agent hash drift when present, then stops and starts so launch prep redeploys the current agent. | +| `BeginAdminRestartAsync(uniqueName, ct)` | Called from `AgentSocketHandler` for `AdminRestart`; sets goal On + AutoStart, marks the runtime state `Restarting`, keeps `IsRestartPending`, and starts immediately if the process already exited. | | `ClearErrorStatusAsync(...)` | Acknowledges a `Crashed`/`Faulted` or stale unhealthy stopped server by flipping goal Off, clearing restart/attach counters and mod-download failures, resetting health, and returning it to `Stopped`. Used by the Dashboard problem banner's `Clear Error Status` action. | | `BeginLauncherDrain()` | Sets `_preserveManagedServersOnShutdown = true` and persists synchronously — called before a worker-only restart so the next worker can re-adopt. | | `Dispose()` | Cancels the persist-debounce CTS and the shutdown CTS. | **`ReconcileAsync`** — per server: liveness vs goal state → Start/Stop/Restart; auto-starts only clean `Stopped` servers while goal is On, leaving `Crashed`/`Faulted` for explicit operator Start; normalizes stale `Stopping`/cancelled `Restarting` states with no active process back to `Stopped`; retries `Starting` processes whose Quasar.Agent attach grace expires when `AutoRestartOnUnhealthy` is enabled; unhealthy auto-restart for running processes (`AutoRestartOnUnhealthy`, throttled by `CanScheduleHealthRestart`); maximum-uptime restart; daily scheduled restart; both planned restarts honour `AvoidSimultaneousScheduledRestarts` via `CanRunPlannedRestart`. Also promotes Starting/Restarting → Running once the agent reports `IsRunning`, applies `ReadyProcessPriority` once healthy, and checks connected running servers for bundled-vs-deployed Quasar.Agent hash drift. Hash drift only logs/sets a status warning; it never auto-schedules a Magnetar restart. The reconcile loop and start path also apply per-server CPU affinity to the live process. -**`HandleProcessExitedAsync` restart budget** — unexpected exits use `RestartOnCrash`, `RestartDelaySeconds`, and `MaxRestartAttempts` (default 3). When the consecutive restart budget is exhausted the state becomes `Faulted` with a restart-limit message, and reconciliation does not keep launching the server until an operator presses Start. +**`HandleProcessExitedAsync` restart budget** — expected admin restart exits (`IsRestartPending` while `Restarting`) relaunch while goal remains `On` and do not consume crash budget. Unexpected exits use `RestartOnCrash`, `RestartDelaySeconds`, and `MaxRestartAttempts` (default 3). When the consecutive restart budget is exhausted the state becomes `Faulted` with a restart-limit message, and reconciliation does not keep launching the server until an operator presses Start. **`RetryAgentAttachAsync` attach budget** — when a launched process remains `Starting` past `AgentStartupGraceSeconds`, the supervisor kills the process, waits `AgentAttachRetryDelaySeconds`, and relaunches. `AgentAttachRetryAttempts` caps the streak (default 3); exhaustion sets `Faulted`. This path depends on health monitoring and `AutoRestartOnUnhealthy`. @@ -45,7 +46,7 @@ Namespace: `Quasar.Services` **`HandleRuntimeWarmupChanged` / `SetStopped`** — `HandleRuntimeWarmupChanged` kicks a reconcile when startup runtime preparation completes, so auto-started servers blocked during SteamCMD/DS download launch promptly after readiness. `SetStopped` records a non-fault stopped state and user-facing message when launch is requested while prerequisites are still preparing. -**`EvaluateHealth` / `EvaluateSimulationProgress`** — agent connectivity, heartbeat staleness, simulation frame-progress score (frames/sec normalised to 60 Hz), uptime warn/recycle thresholds. Honours `DisableServerHealthMonitoring` and per-definition `EnableHealthMonitoring`. Agent-attach grace counts from `AgentWatchSinceUtc`; an expired grace window feeds attach retry while still `Starting` and running-process recovery after promotion. When health monitoring is disabled the per-server health message is now an empty string (the Dashboard surfaces the disabled state once via `HealthMonitoringDisabled`). +**`EvaluateHealth` / `EvaluateSimulationProgress`** — agent connectivity, heartbeat staleness, simulation frame-progress score (frames/sec normalised to 60 Hz), uptime warn/recycle thresholds. Honours `DisableServerHealthMonitoring` and per-definition `EnableHealthMonitoring`. Agent-attach grace counts from `AgentWatchSinceUtc`; an expired grace window feeds attach retry while still `Starting` and running-process recovery after promotion. Collecting the first simulation-progress baseline is reported as `Unknown` rather than `Warning`, so cards can show the state without raising a dashboard warning. When health monitoring is disabled the per-server health message is an empty string (the Dashboard surfaces the disabled state once via `HealthMonitoringDisabled`). **`RestorePersistedRuntimeState` / `TryAdoptProcess`** — on startup, `Process.GetProcessById` re-adopts still-running DS processes from a prior worker, re-attaches the `Exited` handler, and resets `AgentWatchSinceUtc` to "now" so the agent gets a fresh reconnect grace; processes no longer alive are marked Stopped. diff --git a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md index 4c60bd0..165ce81 100644 --- a/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md +++ b/Docs/Reference/files/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs.md @@ -4,7 +4,7 @@ ## Summary -`ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It also exposes a startup readiness workflow that reports SteamCMD, Magnetar, and Dedicated Server check/download/install progress before managed launches are allowed. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time and background checks compare the installed GitHub release tag + asset name with the latest full release and skip archive downloads when that identity is unchanged. Successful GitHub release resolutions are cached in memory for five minutes so a burst of managed server starts does not repeatedly call GitHub. The Magnetar install path branches by OS: Windows ships both runtime builds (`MagnetarInterim.exe` on .NET 10 and `MagnetarLegacy.exe` on .NET Framework 4.8) side-by-side and honors the per-server `DedicatedServerDefinition.ManagedRuntime` selection, while Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. +`ManagedDedicatedServerRuntimeResolver` resolves the paths needed to launch a dedicated server — the Magnetar launcher executable, its working directory, the `DedicatedServer64` directory, and any native-library search paths required by the child process — and auto-installs Magnetar, SteamCMD, and the DS itself when absent. It exposes startup readiness plus separate manual/current checks for Magnetar and the Dedicated Server, reports installed versions/paths to the warmup/update UI, and includes versions in progress events. It supports `.zip`, `.tar.gz`, and `.7z` archives for both Magnetar and SteamCMD downloads, guarded by per-component `SemaphoreSlim` install locks. Managed Magnetar installs are tracked by a `.quasar-magnetar-release.json` marker, so launch-time/background checks compare installed GitHub release tag + asset name with the latest full release and skip archive downloads when unchanged. Successful GitHub release resolutions are cached in memory for five minutes. Windows ships both runtime builds side-by-side; Linux ships a single Interim build behind a top-level wrapper with the apphost under `Bin/`. ## Structure @@ -17,6 +17,8 @@ Namespace: `Quasar.Services` | `ResolveAsync(DedicatedServerDefinition, ct)` | Entry point: picks the runtime flavor (`definition.ManagedRuntime` on Windows, forced `DotNet10` elsewhere); if the configured executable looks like the DS itself (or is empty) → ensure managed Magnetar install; otherwise validate the custom launcher path. Picks working directory, resolves `DedicatedServer64Path` and Linux native-library search paths, returns `ResolvedDedicatedServerRuntime`. | | `EnsureManagedRuntimeReadyAsync(progress?, ct)` | Startup readiness path: ensures managed SteamCMD, validates/downloads the managed Dedicated Server install, prepares Linux SteamCMD `linux64` native runtime when needed, reports component progress, and returns `ManagedRuntimeReadiness`. | | `EnsureManagedMagnetarCurrentAsync(progress?, ct)` | Public background-check hook used by `ManagedRuntimeWarmupService`; ensures the managed Magnetar install is present and current with the latest configured archive source while reporting component progress. | +| `EnsureManagedDedicatedServerCurrentAsync(progress?, ct)` | Public manual-check hook used by the Updates page through `ManagedRuntimeWarmupService`; ensures SteamCMD is present, runs DS install/update validation, and reports SteamCMD/DS readiness with versions. | +| `GetInstalledVersions()` | Returns `ManagedRuntimeVersionSnapshot` with currently resolvable SteamCMD, Magnetar, and Dedicated Server paths/versions without running an update. | | `EnsureManagedMagnetarInstallAsync(runtime, progress?, ct)` | Dispatcher: routes to the Windows or Linux install method by `OperatingSystem.IsWindows()`. | | `EnsureLinuxManagedMagnetarInstallAsync(progress?, ct)` | Linux path: reports Magnetar checking/readiness, resolves the latest archive reference, compares its stable identity with the installed marker, and returns the apphost binary directly under `/Bin/` when current; otherwise downloads/extracts the archive with progress, copies `Bin/`, sets exec bit, writes the marker, and is locked by `_magnetarInstallLock`. | | `EnsureWindowsManagedMagnetarInstallAsync(runtime, progress?, ct)` | Windows path: reports Magnetar checking/readiness, resolves the latest archive reference, compares its stable identity with the installed marker, and returns the requested launcher exe (`GetWindowsMagnetarLauncherFileName`) when current; otherwise installs both builds together into one folder and writes the marker. Its containing folder is the working directory (holds the `Libraries` payload). Locked by `_magnetarInstallLock`. | @@ -39,7 +41,9 @@ Namespace: `Quasar.Services` **`ManagedRuntimeReadiness`** — sealed record returned by startup readiness checks: readiness bool, SteamCMD path, SteamCMD runtime path, DedicatedServer64 path, and failure message. -**`ManagedRuntimeInstallProgress`** — sealed record emitted to readiness progress sinks with component (`SteamCmd` / `Magnetar` / `DedicatedServer`), phase (`Pending`, `Checking`, `Downloading`, `Installing`, `Ready`, `Failed`), message, optional percent, and path. +**`ManagedRuntimeVersionSnapshot`** — sealed record carrying installed SteamCMD, Magnetar, and Dedicated Server paths plus version strings derived from marker metadata or executable/assembly file versions. + +**`ManagedRuntimeInstallProgress`** — sealed record emitted to readiness progress sinks with component (`SteamCmd` / `Magnetar` / `DedicatedServer`), phase (`Pending`, `Checking`, `Downloading`, `Installing`, `Ready`, `Failed`), message, optional percent, path, and version. Internal enum `ArchiveKind` (`Unknown`, `Zip`, `TarGz`, `SevenZip`). Private Magnetar metadata helpers include `MagnetarSource`, `MagnetarArchiveReference`, `InstalledMagnetarRelease`, and `MagnetarArchiveSourceKinds`. @@ -54,4 +58,4 @@ Internal enum `ArchiveKind` (`Unknown`, `Zip`, `TarGz`, `SevenZip`). Private Mag ## 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. The stored download URL is retained for diagnostics and direct-URL overrides, but GitHub URL churn alone does not invalidate the cache. 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 (no `Bin/` wrapper), so the resolved launcher sits directly in the install root and its folder is the working directory; 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` so a `server.json` moved across platforms still launches. 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` so `steamclient.so` resolution does not depend on a desktop Steam install. `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 executable/assembly version when available. 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/Reference/files/Quasar/Services/ManagedRuntimeWarmupService.cs.md b/Docs/Reference/files/Quasar/Services/ManagedRuntimeWarmupService.cs.md index e50cc16..efc7f56 100644 --- a/Docs/Reference/files/Quasar/Services/ManagedRuntimeWarmupService.cs.md +++ b/Docs/Reference/files/Quasar/Services/ManagedRuntimeWarmupService.cs.md @@ -4,7 +4,7 @@ ## Summary -`ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until those prerequisites are ready. After the startup warmup, it checks the managed Magnetar install for updates every hour and feeds Magnetar checking/download/install progress into the visible component snapshot. It exposes a component-level `ManagedRuntimeWarmupSnapshot` for dashboard progress display and fires a `Changed` event on every status update. +`ManagedRuntimeWarmupService` is a `BackgroundService` that immediately checks and prepares the managed SteamCMD, Magnetar, and Space Engineers Dedicated Server installs at Quasar startup, so managed launches are blocked until prerequisites are ready. After startup it checks the managed Magnetar install for updates every hour, exposes manual Magnetar and Dedicated Server check methods for the Updates page, enriches snapshots with installed versions/paths, and fires `Changed` on every status update. ## Structure @@ -15,13 +15,16 @@ Namespace: `Quasar.Services` | Member | Description | |---|---| | `event Action? Changed` | Raised on state transitions. | -| `GetSnapshot()` | Returns a copy of the current `ManagedRuntimeWarmupSnapshot`. | +| `GetSnapshot()` | Returns a copy of the current `ManagedRuntimeWarmupSnapshot`, enriched with installed runtime paths/versions from the resolver. | | `bool IsReady` | True only after SteamCMD, Magnetar, and Dedicated Server readiness completes. Used by `DedicatedServerSupervisor` to gate managed server launches. | | `BlockLaunchMessage` | User-facing reason shown when a launch is requested before managed runtime readiness. | | `RetryAsync(ct)` | Reruns the same readiness workflow for dashboard retry after a failed Dedicated Server download. A semaphore prevents concurrent warmups. | | `ExecuteAsync(ct)` | Transitions `Pending → Running`, calls `_runtimeResolver.EnsureManagedRuntimeReadyAsync` with a progress reporter, then transitions to `Complete` or `Failed`; after that, runs an hourly `PeriodicTimer` for managed Magnetar update checks. | +| `CheckMagnetarNowAsync(ct)` | Public UI hook that runs the managed Magnetar update check immediately. | +| `CheckDedicatedServerNowAsync(ct)` | Public UI hook that runs a managed Dedicated Server update/validate check immediately. | | `RunMagnetarUpdateCheckAsync(ct)` | Uses the same semaphore as warmup/retry, calls `_runtimeResolver.EnsureManagedMagnetarCurrentAsync` with a progress reporter, and logs update-check failures without faulting the readiness snapshot. | -| `ApplyProgress(...)` | Maps resolver progress events into per-component snapshot rows and raises `Changed` for live UI refresh. | +| `RunDedicatedServerUpdateCheckAsync(ct)` | Uses the same semaphore, calls `_runtimeResolver.EnsureManagedDedicatedServerCurrentAsync`, and records failures on the Dedicated Server component row. | +| `ApplyProgress(...)` | Maps resolver progress events into per-component snapshot rows, including version and last-check timestamps, and raises `Changed` for live UI refresh. | **`ManagedRuntimeWarmupState`** — enum `{Pending, Running, Complete, Failed}`. @@ -29,8 +32,8 @@ Namespace: `Quasar.Services` **`ManagedRuntimeComponentState`** — enum `{Pending, Checking, Downloading, Installing, Ready, Failed}`. -**`ManagedRuntimeComponentSnapshot`** — sealed record carrying component id, display name, state, message, optional percent, and path. +**`ManagedRuntimeComponentSnapshot`** — sealed record carrying component id, display name, state, message, optional percent, path, installed version, and last check timestamp. ## Dependencies -- [`Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs`](ManagedDedicatedServerRuntimeResolver.cs.md) — `EnsureManagedRuntimeReadyAsync`, `EnsureManagedMagnetarCurrentAsync`, progress DTOs +- [`Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs`](ManagedDedicatedServerRuntimeResolver.cs.md) — `EnsureManagedRuntimeReadyAsync`, `EnsureManagedMagnetarCurrentAsync`, `EnsureManagedDedicatedServerCurrentAsync`, `GetInstalledVersions`, progress DTOs diff --git a/Docs/StateMachines/DedicatedServerLifecycle.md b/Docs/StateMachines/DedicatedServerLifecycle.md index 7aed7cf..17f5cc1 100644 --- a/Docs/StateMachines/DedicatedServerLifecycle.md +++ b/Docs/StateMachines/DedicatedServerLifecycle.md @@ -21,17 +21,21 @@ observed state. ## Goal state The desired state. Operator actions usually mutate goal state first, then let -reconciliation perform the transition. An in-game admin `!quit` or Quasar Agent -`!stop` is reported by the agent as an `AdminStop` signal so Quasar flips the -goal to `Off` (and therefore does **not** treat the shutdown as a crash to -restart). +reconciliation perform the transition. Quasar Agent owns the in-game `!stop`, +`!quit`, and `!restart` roots for managed servers. `!stop` saves and reports +`AdminStop`; `!quit` reports `AdminStop` and exits immediately without saving. +Both flip the goal to `Off` (and therefore do **not** treat the shutdown as a +crash to restart). `!restart` reports `AdminRestart`, keeps the goal `On`, moves +the observed process to `Restarting`, and lets Quasar relaunch after the process +exits. ```mermaid stateDiagram-v2 [*] --> Off Off --> On: operator Start / SetGoalStateAsync(On) On --> Off: operator Stop / SetGoalStateAsync(Off) - On --> Off: in-game admin !quit / Quasar !stop (AdminStop signal) + On --> Off: Quasar Agent !stop / !quit (AdminStop signal) + On --> On: Quasar Agent !restart (AdminRestart signal) On --> Off: Discord !stop command ``` @@ -41,7 +45,8 @@ stateDiagram-v2 | --- | --- | --- | | `Off → On` | Operator/API `SetGoalStateAsync(On)` | `DedicatedServerSupervisor.SetGoalStateAsync` | | `On → Off` | Operator/API `SetGoalStateAsync(Off)` | `DedicatedServerSupervisor.SetGoalStateAsync` | -| `On → Off` | In-game admin `!quit` / Quasar Agent `!stop` → agent `AdminStop` | `AgentSocketHandler.ProcessMessageAsync` (`AdminStop` case) | +| `On → Off` | Quasar Agent `!stop` / `!quit` → agent `AdminStop` | `AgentSocketHandler.ProcessMessageAsync` (`AdminStop` case) | +| `On → On` | Quasar Agent `!restart` → agent `AdminRestart` | `AgentSocketHandler.ProcessMessageAsync` (`AdminRestart` case), `DedicatedServerSupervisor.BeginAdminRestartAsync` | | `On → Off` | Discord `!stop` command | `DiscordCommandDispatcher.DispatchAsync` | --- @@ -127,8 +132,11 @@ stateDiagram-v2 `AgentAttachRetryAttempts` consecutive attach retries, the server becomes `Faulted`. - Planned restarts come from the health policy (`Unhealthy` + - `AutoRestartOnUnhealthy`), `MaximumUptime`, and `DailyRestartTimeLocal` - (optionally staggered by `AvoidSimultaneousScheduledRestarts`). + `AutoRestartOnUnhealthy`), `MaximumUptime`, `DailyRestartTimeLocal` + (optionally staggered by `AvoidSimultaneousScheduledRestarts`), and the + Quasar Agent `!restart` command. Agent-requested restart is tracked as + `Restarting` before the process exits; the subsequent clean exit is relaunched + without consuming crash-restart budget. --- @@ -169,6 +177,9 @@ The simulation-frame check mirrors the dedicated server's own watcher: `frameProgressScore = deltaFrames / (elapsedSeconds * 60)` is compared against a configurable minimum, with save-in-progress windows resetting the baseline instead of counting as a stall (`EvaluateSimulationProgress`). +Collecting the first simulation-progress baseline is `Unknown`, not `Warning`; +the card can show the state, but it does not raise a dashboard warning +notification. --- diff --git a/Docs/WindowsDeploymentAndUpdates.md b/Docs/WindowsDeploymentAndUpdates.md index 4a4c83f..8b9b346 100644 --- a/Docs/WindowsDeploymentAndUpdates.md +++ b/Docs/WindowsDeploymentAndUpdates.md @@ -104,6 +104,21 @@ release file without local appsettings values. During activation, the resolved file is copied back to the install directory so Bootstrap and the managed worker launch with the same base settings. +## Managed Runtime Update Checks + +The Updates page always shows the currently installed Quasar, Bootstrap, +Magnetar, and Space Engineers Dedicated Server versions when Quasar can resolve +them from release metadata or executable file versions. It also shows the +managed runtime install paths and the most recent managed-runtime check time. + +Quasar UI worker and Bootstrap checks use the Quasar release checker interval +(15 minutes by default) and the page's **Check Quasar** button. Managed Magnetar +checks run during startup readiness and then every hour while Quasar is running; +the page's **Check Magnetar** button runs the same check immediately. Managed DS +checks run during startup readiness; **Check Dedicated Server** runs SteamCMD +`app_update 298740 validate` immediately so an operator does not need to wait +for a restart to verify or refresh the DS install. + ## Bootstrap Updates Bootstrap checks the primary Quasar release stream every 15 minutes. When it diff --git a/Magnetar.Protocol/Transport/WireMessageKind.cs b/Magnetar.Protocol/Transport/WireMessageKind.cs index 4157150..8c19658 100644 --- a/Magnetar.Protocol/Transport/WireMessageKind.cs +++ b/Magnetar.Protocol/Transport/WireMessageKind.cs @@ -11,5 +11,6 @@ public static class WireMessageKind public const string PluginConfigSnapshot = "plugin-config-snapshot"; public const string PluginConfigUpdate = "plugin-config-update"; public const string AdminStop = "admin-stop"; + public const string AdminRestart = "admin-restart"; public const string PluginLogs = "plugin-logs"; } diff --git a/Quasar.Agent/AdminPlugin.cs b/Quasar.Agent/AdminPlugin.cs index 02d11f6..c6cba0d 100644 --- a/Quasar.Agent/AdminPlugin.cs +++ b/Quasar.Agent/AdminPlugin.cs @@ -1,5 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; using Magnetar.Protocol.Model; using PluginSdk; using PluginSdk.Commands; @@ -7,6 +11,7 @@ using Sandbox.Game.World; using VRage.Game.ModAPI; using VRage.Plugins; +using VRage.Utils; namespace Quasar.Agent { @@ -21,15 +26,19 @@ public class AdminPlugin : IPlugin private AgentConnection _connection; private PluginLogOutbox _outbox; private readonly object _adminStopSync = new object(); + private readonly object _adminRestartSync = new object(); private bool _adminStopReported; + private bool _adminRestartRequested; + private bool _adminRestartReported; private DateTime _lastDeathSubscriptionRefreshUtc = DateTime.MinValue; public void Init(object gameServer) { var options = AgentOptions.FromEnvironment(); + LogStartupVersions(); AgentProfiler.Configure(options); AgentProfilerPatches.Apply(options); - ServerCommands.Register(typeof(AdminPlugin).Assembly, typeof(StopCommand)); + ServerCommands.Register(typeof(AdminPlugin).Assembly, typeof(StopCommand), typeof(RestartCommand), typeof(QuitCommand)); _bridge = new GameBridge(gameServer); // Start capturing plugin log lines before the connection loop so any @@ -39,6 +48,8 @@ public void Init(object gameServer) _connection = new AgentConnection(_bridge, new WebServiceLocator(), options, _outbox); StopCommand.AdminStopRequested = ReportAdminStop; + QuitCommand.AdminStopRequested = ReportAdminStop; + RestartCommand.AdminRestartRequested = ReportAdminRestart; _connection.Start(); MyVisualScriptLogicProvider.PlayerDied += OnPlayerDied; ServerControl.Terminating += OnServerTerminating; @@ -54,6 +65,8 @@ public void Dispose() { ServerControl.Terminating -= OnServerTerminating; StopCommand.AdminStopRequested = null; + QuitCommand.AdminStopRequested = null; + RestartCommand.AdminRestartRequested = null; MyVisualScriptLogicProvider.PlayerDied -= OnPlayerDied; UnsubscribeDeathHandlers(); _connection?.Stop(); @@ -74,7 +87,8 @@ private void OnServerTerminating(ServerTerminationKind kind) { if (kind == ServerTerminationKind.Shutdown && _bridge != null && - !_bridge.QuasarRequestedStop) + !_bridge.QuasarRequestedStop && + !IsAdminRestartRequested()) { ReportAdminStop(); } @@ -82,6 +96,9 @@ private void OnServerTerminating(ServerTerminationKind kind) private void ReportAdminStop() { + if (IsAdminRestartRequested()) + return; + lock (_adminStopSync) { if (_adminStopReported) @@ -91,6 +108,113 @@ private void ReportAdminStop() } } + private void ReportAdminRestart() + { + lock (_adminRestartSync) + { + _adminRestartRequested = true; + if (_adminRestartReported) + return; + + _adminRestartReported = _connection?.TrySendAdminRestart() == true; + } + } + + private bool IsAdminRestartRequested() + { + lock (_adminRestartSync) + { + return _adminRestartRequested; + } + } + + private static void LogStartupVersions() + { + var magnetarVersion = ResolveMagnetarVersion(); + var agentVersion = GetAssemblyVersion(typeof(AdminPlugin).Assembly); + var message = $"Quasar.Agent startup: Magnetar={magnetarVersion}; Quasar.Agent={agentVersion}."; + + try + { + MyLog.Default?.WriteLineAndConsole(message); + } + catch + { + } + + try + { + Console.WriteLine($"[Quasar.Agent] {message}"); + } + catch + { + } + } + + private static string ResolveMagnetarVersion() + { + var entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly != null && IsMagnetarAssembly(entryAssembly)) + return GetAssemblyVersion(entryAssembly); + + var loadedMagnetarAssembly = AppDomain.CurrentDomain.GetAssemblies() + .Where(IsMagnetarAssembly) + .OrderBy(assembly => assembly.GetName().Name, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + if (loadedMagnetarAssembly != null) + return GetAssemblyVersion(loadedMagnetarAssembly); + + return GetMagnetarProcessVersion(); + } + + private static bool IsMagnetarAssembly(Assembly assembly) + { + var name = assembly.GetName().Name; + return !string.IsNullOrWhiteSpace(name) && + name.StartsWith("Magnetar", StringComparison.OrdinalIgnoreCase); + } + + private static string GetAssemblyVersion(Assembly assembly) + { + return assembly.GetCustomAttribute()?.InformationalVersion + ?? assembly.GetName().Version?.ToString() + ?? "unknown"; + } + + private static string GetMagnetarProcessVersion() + { + try + { + using (var process = Process.GetCurrentProcess()) + { + var fileName = process.MainModule?.FileName; + if (string.IsNullOrWhiteSpace(fileName) || + !Path.GetFileName(fileName).StartsWith("Magnetar", StringComparison.OrdinalIgnoreCase)) + { + return "unknown"; + } + + var version = FileVersionInfo.GetVersionInfo(fileName); + return FirstNonEmpty(version.ProductVersion, version.FileVersion, "unknown"); + } + } + catch + { + return "unknown"; + } + } + + private static string FirstNonEmpty(params string[] values) + { + foreach (var value in values) + { + if (!string.IsNullOrWhiteSpace(value)) + return value; + } + + return string.Empty; + } + private void OnPlayerDied(long identityId) { RecordDeath(identityId, ResolveVictimName(identityId, null, null)); diff --git a/Quasar.Agent/AgentConnection.cs b/Quasar.Agent/AgentConnection.cs index aef1c37..3e9c5bd 100644 --- a/Quasar.Agent/AgentConnection.cs +++ b/Quasar.Agent/AgentConnection.cs @@ -225,6 +225,21 @@ private TimeSpan NextReconnectDelay() /// open and before the process exits. Never hangs shutdown. /// public bool TrySendAdminStop() + { + return TrySendAdminSignal(WireMessageKind.AdminStop, "admin-stop"); + } + + /// + /// Best-effort signal sent when an admin requested an in-game restart. + /// Quasar keeps the goal state On and moves the server into Restarting + /// before the process exits. + /// + public bool TrySendAdminRestart() + { + return TrySendAdminSignal(WireMessageKind.AdminRestart, "admin-restart"); + } + + private bool TrySendAdminSignal(string kind, string label) { var socket = _socket; if (socket == null || socket.State != WebSocketState.Open) @@ -234,12 +249,12 @@ public bool TrySendAdminStop() { return SendAsync(socket, new AgentWireMessage { - Kind = WireMessageKind.AdminStop, + Kind = kind, }, CancellationToken.None).Wait(TimeSpan.FromSeconds(2)); } catch (Exception exception) { - Log($"Failed sending admin-stop signal: {exception.Message}"); + Log($"Failed sending {label} signal: {exception.Message}"); return false; } } diff --git a/Quasar.Agent/StopCommand.cs b/Quasar.Agent/StopCommand.cs index 8fe4608..67c281b 100644 --- a/Quasar.Agent/StopCommand.cs +++ b/Quasar.Agent/StopCommand.cs @@ -32,4 +32,60 @@ private static void TryNotifyAdminStopRequested() } } } + + [CommandRoot("restart", "Quasar", "Save the world then restart the server")] + public sealed class RestartCommand : CommandModule + { + internal static Action AdminRestartRequested { get; set; } + + [Command("", "Save the world then restart the server")] + public void Restart() + { + Context.Respond("Saving world and restarting the server..."); + Task.Run(() => + { + TryNotifyAdminRestartRequested(); + ServerControl.SaveAndQuit(); + }); + } + + private static void TryNotifyAdminRestartRequested() + { + try + { + AdminRestartRequested?.Invoke(); + } + catch + { + } + } + } + + [CommandRoot("quit", "Quasar", "Quit the server immediately without saving")] + public sealed class QuitCommand : CommandModule + { + internal static Action AdminStopRequested { get; set; } + + [Command("", "Quit the server immediately without saving")] + public void Quit() + { + Context.Respond("Quitting without saving..."); + Task.Run(() => + { + TryNotifyAdminStopRequested(); + ServerControl.QuitWithoutSaving(); + }); + } + + private static void TryNotifyAdminStopRequested() + { + try + { + AdminStopRequested?.Invoke(); + } + catch + { + } + } + } } diff --git a/Quasar/Components/Dashboard/ServerCard.razor b/Quasar/Components/Dashboard/ServerCard.razor index 44e689b..301c78a 100644 --- a/Quasar/Components/Dashboard/ServerCard.razor +++ b/Quasar/Components/Dashboard/ServerCard.razor @@ -1,4 +1,8 @@ @inject ServerManagementActions ServerActions +@inject QuasarConfigProfileCatalog ConfigProfiles +@inject IJSRuntime JS +@inject ISnackbar Snackbar +@inject NavigationManager Navigation @@ -97,6 +101,34 @@ { @Runtime.HealthSummary } + + + + Port @Server.ServerPort + + + @if (CanOpenConfigProfile) + { + + + Config @GetConfigProfileName() + + + } + else + { + + Config @GetConfigProfileName() + + } + @@ -132,6 +164,9 @@ [Parameter] public EventCallback RestartRequested { get; set; } + [Parameter] + public EventCallback ConfigProfileSelected { get; set; } + private DedicatedServerProcessState ProcessState => Runtime?.State ?? DedicatedServerProcessState.Stopped; private bool IsProcessActive => ProcessState is DedicatedServerProcessState.Starting @@ -152,6 +187,10 @@ private bool CanCreateTemplate => ServerActions.CanCreateWorldTemplate(Server); + private bool CanOpenConfigProfile => + !string.IsNullOrWhiteSpace(Server.ConfigProfileId) && + ConfigProfiles.GetProfile(Server.ConfigProfileId) is not null; + private Task StartAsync() => StartRequested.InvokeAsync(Server.UniqueName); private Task StopAsync() => StopRequested.InvokeAsync(Server.UniqueName); @@ -170,6 +209,23 @@ private Task DeleteAsync() => ServerActions.DeleteAsync(Server.UniqueName); + private async Task OpenConfigProfileAsync() + { + if (!CanOpenConfigProfile) + return; + + await ConfigProfileSelected.InvokeAsync(Server.ConfigProfileId); + } + + private async Task CopyDirectConnectAsync() + { + var address = GetDirectConnectAddress(); + var ok = await JS.InvokeAsync("quasarConfigs.copyText", address); + Snackbar.Add( + ok ? $"Copied {address} to clipboard." : "Copy failed - select and copy the port manually.", + ok ? Severity.Info : Severity.Warning); + } + private string GetDisplayName() { if (!string.IsNullOrWhiteSpace(Server.DisplayName)) @@ -203,6 +259,52 @@ return "World pending"; } + private string GetConfigProfileName() + { + if (string.IsNullOrWhiteSpace(Server.ConfigProfileId)) + return "-"; + + return ConfigProfiles.GetProfile(Server.ConfigProfileId)?.Name ?? $"Missing: {Server.ConfigProfileId}"; + } + + private string GetDirectConnectTooltip() => $"Copy direct connect address {GetDirectConnectAddress()}"; + + private string GetDirectConnectAddress() + { + var fallbackHost = Navigation.ToAbsoluteUri(Navigation.BaseUri).Host; + var host = ResolveDirectConnectHost(Server.ServerIP, fallbackHost); + if (host.Contains(':', StringComparison.Ordinal) && + !host.StartsWith("[", StringComparison.Ordinal) && + !host.EndsWith("]", StringComparison.Ordinal)) + { + host = $"[{host}]"; + } + + return $"{host}:{Server.ServerPort}"; + } + + private static string ResolveDirectConnectHost(string? configuredHost, string fallbackHost) + { + var host = configuredHost?.Trim() ?? string.Empty; + if (IsAnyAddress(host)) + host = fallbackHost?.Trim() ?? string.Empty; + + if (IsAnyAddress(host) || string.IsNullOrWhiteSpace(host)) + return "127.0.0.1"; + + return host; + } + + private static bool IsAnyAddress(string value) + { + var normalized = value.Trim().Trim('[', ']'); + return string.IsNullOrWhiteSpace(normalized) || + string.Equals(normalized, "0.0.0.0", StringComparison.Ordinal) || + string.Equals(normalized, "::", StringComparison.Ordinal) || + string.Equals(normalized, "*", StringComparison.Ordinal) || + string.Equals(normalized, "+", StringComparison.Ordinal); + } + private string GetStatusLabel() { if (ProcessState == DedicatedServerProcessState.Stopping) diff --git a/Quasar/Components/Pages/Home.razor b/Quasar/Components/Pages/Home.razor index 11fe0ed..b4e0aad 100644 --- a/Quasar/Components/Pages/Home.razor +++ b/Quasar/Components/Pages/Home.razor @@ -10,6 +10,7 @@ @inject IDialogService DialogService @inject ISnackbar Snackbar @inject NavigationManager Navigation +@inject ILocalStorageService LocalStorage @(IsListView ? "Servers" : "Dashboard") @@ -474,16 +475,25 @@ Servers + @if (IsCardsView) + { + + Create Server + + } + OnClick="@(() => SetServerViewAsync(DashboardServerView.Cards))"> Cards + OnClick="@(() => SetServerViewAsync(DashboardServerView.List))"> List @@ -521,7 +531,8 @@ else StartRequested="StartAsync" StopRequested="StopAsync" KillStartingRequested="KillStartingAsync" - RestartRequested="RestartAsync" /> + RestartRequested="RestartAsync" + ConfigProfileSelected="OpenConfigProfileFromServerListAsync" /> } @@ -529,6 +540,8 @@ else @code { + private const string ServerViewStorageKey = "quasar.dashboard.serverView"; + private enum DashboardServerView { Cards, @@ -552,9 +565,9 @@ else private IReadOnlyList LaunchedServers => Servers.Where(server => IsRunning(server.UniqueName)).ToList(); private bool IsLaunchBlocked => RuntimeWarmupSnapshot.State != ManagedRuntimeWarmupState.Complete; private bool ShowDataHandlingConsentPrompt => DataHandlingConsent.GetSettings().ConsentGranted is null; - private DashboardServerView ServerView => string.Equals(ServerViewQuery, "list", StringComparison.OrdinalIgnoreCase) - ? DashboardServerView.List - : DashboardServerView.Cards; + private DashboardServerView _serverView = DashboardServerView.Cards; + private bool _serverViewLoadedFromStorage; + private DashboardServerView ServerView => _serverView; private bool IsCardsView => ServerView == DashboardServerView.Cards; private bool IsListView => ServerView == DashboardServerView.List; @@ -705,6 +718,39 @@ else _setupWizardActive = ConfiguredServerCount == 0; } + protected override void OnParametersSet() + { + if (TryParseServerView(ServerViewQuery, out var queryView)) + { + _serverView = queryView; + _serverViewLoadedFromStorage = true; + _ = SaveServerViewPreferenceAsync(queryView); + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender || _serverViewLoadedFromStorage || TryParseServerView(ServerViewQuery, out _)) + return; + + _serverViewLoadedFromStorage = true; + try + { + var stored = await LocalStorage.GetItemAsync(ServerViewStorageKey); + if (TryParseServerView(stored, out var storedView) && storedView != _serverView) + { + _serverView = storedView; + await InvokeAsync(StateHasChanged); + } + } + catch (InvalidOperationException) + { + } + catch (JSDisconnectedException) + { + } + } + public void Dispose() { Registry.Changed -= HandleRegistryChanged; @@ -769,8 +815,15 @@ else private Task OpenImportWorldTemplateDialogAsync() => ShowFullScreenPageDialogAsync("World Templates"); - private Task OpenCreateServerDialogAsync() => - ShowFullScreenPageDialogAsync("Servers"); + private Task OpenCreateServerDialogAsync() + { + var parameters = new DialogParameters + { + [nameof(ServersPageDialog.OpenCreateOnStart)] = true, + }; + + return ShowFullScreenPageDialogAsync("Servers", parameters); + } private Task OpenConfigProfileFromServerListAsync(string configProfileId) { @@ -782,12 +835,46 @@ else return ShowFullScreenPageDialogAsync("Config Profiles", parameters); } - private void SetServerView(DashboardServerView view) + private async Task SetServerViewAsync(DashboardServerView view) { + _serverView = view; + await SaveServerViewPreferenceAsync(view); var target = view == DashboardServerView.List ? "/?view=list" : "/"; Navigation.NavigateTo(target, replace: true); } + private async Task SaveServerViewPreferenceAsync(DashboardServerView view) + { + try + { + await LocalStorage.SetItemAsync(ServerViewStorageKey, view == DashboardServerView.List ? "list" : "cards"); + } + catch (InvalidOperationException) + { + } + catch (JSDisconnectedException) + { + } + } + + private static bool TryParseServerView(string? value, out DashboardServerView view) + { + if (string.Equals(value, "list", StringComparison.OrdinalIgnoreCase)) + { + view = DashboardServerView.List; + return true; + } + + if (string.Equals(value, "cards", StringComparison.OrdinalIgnoreCase)) + { + view = DashboardServerView.Cards; + return true; + } + + view = DashboardServerView.Cards; + return false; + } + private async Task ShowFullScreenPageDialogAsync(string title, DialogParameters? parameters = null) where TDialog : ComponentBase { var options = new DialogOptions diff --git a/Quasar/Components/Pages/Servers.razor b/Quasar/Components/Pages/Servers.razor index c7cfa9c..2383320 100644 --- a/Quasar/Components/Pages/Servers.razor +++ b/Quasar/Components/Pages/Servers.razor @@ -8,6 +8,7 @@ @inject WebServiceOptions Options @inject ISnackbar Snackbar @inject NavigationManager Navigation +@inject IJSRuntime JS @if (!Embedded) { @@ -65,7 +66,16 @@ @context.UniqueName - @context.ServerPort + + + + @context.ServerPort + + + @if (CanOpenConfigProfile(context.ConfigProfileId)) { @@ -169,6 +179,7 @@ { private readonly HashSet _expanded = new(StringComparer.OrdinalIgnoreCase); private bool _creatingWorldTemplate; + private bool _createOpenedFromParameter; private enum CloneWorldMode { @@ -179,6 +190,9 @@ [Parameter] public EventCallback ConfigProfileSelected { get; set; } + [Parameter] + public bool OpenCreateOnStart { get; set; } + [Parameter] public bool Embedded { get; set; } @@ -234,6 +248,15 @@ WorldTemplates.Changed += HandleChanged; } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!OpenCreateOnStart || _createOpenedFromParameter) + return; + + _createOpenedFromParameter = true; + await OpenCreateDialogAsync(); + } + public void Dispose() { ServerCatalog.Changed -= HandleChanged; @@ -335,6 +358,15 @@ Navigation.NavigateTo($"/configs?profileId={Uri.EscapeDataString(configProfileId)}"); } + private async Task CopyDirectConnectAsync(DedicatedServerDefinition definition) + { + var address = GetDirectConnectAddress(definition); + var ok = await JS.InvokeAsync("quasarConfigs.copyText", address); + Snackbar.Add( + ok ? $"Copied {address} to clipboard." : "Copy failed - select and copy the port manually.", + ok ? Severity.Info : Severity.Warning); + } + private async Task OpenConsoleDialogAsync(string uniqueName) { var parameters = new DialogParameters @@ -812,6 +844,45 @@ !string.IsNullOrWhiteSpace(configProfileId) && ConfigProfiles.GetProfile(configProfileId) is not null; + private string GetDirectConnectTooltip(DedicatedServerDefinition definition) => + $"Copy direct connect address {GetDirectConnectAddress(definition)}"; + + private string GetDirectConnectAddress(DedicatedServerDefinition definition) + { + var fallbackHost = Navigation.ToAbsoluteUri(Navigation.BaseUri).Host; + var host = ResolveDirectConnectHost(definition.ServerIP, fallbackHost); + if (host.Contains(':', StringComparison.Ordinal) && + !host.StartsWith("[", StringComparison.Ordinal) && + !host.EndsWith("]", StringComparison.Ordinal)) + { + host = $"[{host}]"; + } + + return $"{host}:{definition.ServerPort}"; + } + + private static string ResolveDirectConnectHost(string? configuredHost, string fallbackHost) + { + var host = configuredHost?.Trim() ?? string.Empty; + if (IsAnyAddress(host)) + host = fallbackHost?.Trim() ?? string.Empty; + + if (IsAnyAddress(host) || string.IsNullOrWhiteSpace(host)) + return "127.0.0.1"; + + return host; + } + + private static bool IsAnyAddress(string value) + { + var normalized = value.Trim().Trim('[', ']'); + return string.IsNullOrWhiteSpace(normalized) || + string.Equals(normalized, "0.0.0.0", StringComparison.Ordinal) || + string.Equals(normalized, "::", StringComparison.Ordinal) || + string.Equals(normalized, "*", StringComparison.Ordinal) || + string.Equals(normalized, "+", StringComparison.Ordinal); + } + private string GetStateText(string uniqueName) { var runtime = GetRuntime(uniqueName); diff --git a/Quasar/Components/Pages/ServersPageDialog.razor b/Quasar/Components/Pages/ServersPageDialog.razor index 7654243..cf28fb4 100644 --- a/Quasar/Components/Pages/ServersPageDialog.razor +++ b/Quasar/Components/Pages/ServersPageDialog.razor @@ -6,7 +6,8 @@ - + Done @@ -21,6 +22,9 @@ [Inject] private IDialogService DialogService { get; set; } = default!; + [Parameter] + public bool OpenCreateOnStart { get; set; } + private void Close() => MudDialog.Close(); private async Task OpenConfigProfileAsync(string configProfileId) diff --git a/Quasar/Components/Pages/Updates.razor b/Quasar/Components/Pages/Updates.razor index f8a5c3d..ea51c3a 100644 --- a/Quasar/Components/Pages/Updates.razor +++ b/Quasar/Components/Pages/Updates.razor @@ -4,6 +4,7 @@ @inject QuasarUpdateService UpdateService @inject QuasarUpdateOptions Options @inject WebServiceOptions WebOptions +@inject ManagedRuntimeWarmupService RuntimeWarmup @inject ISnackbar Snackbar @inject IDialogService DialogService @inject IJSRuntime JS @@ -19,7 +20,7 @@ StartIcon="@Icons.Material.Filled.Upgrade" Disabled="_busy" OnClick="CheckNowAsync"> - Check now + Check Quasar @@ -191,6 +192,63 @@ + + + + +
+ Managed Runtime + + Magnetar checks run every @ManagedRuntimeWarmupService.MagnetarUpdateCheckPeriod.TotalMinutes.ToString("0") min. Dedicated Server checks run at startup and when requested here. + +
+ + + Check Magnetar + + + Check Dedicated Server + + +
+ + + + Component + Installed version + Status + Path + Last check + + + @context.DisplayName + @FormatRuntimeVersion(context) + @context.Message + + @if (!string.IsNullOrWhiteSpace(context.Path)) + { + + } + else + { + - + } + + @FormatTimestamp(context.LastCheckedUtc) + + +
+
+
+ @@ -238,7 +296,9 @@ @code { private QuasarUpdateSnapshot _snapshot = new(); + private ManagedRuntimeWarmupSnapshot _runtimeSnapshot = ManagedRuntimeWarmupSnapshot.CreateInitial(); private bool _busy; + private bool _runtimeBusy; private bool _includePrerelease; private bool _autoStageWebUpdates = true; private bool _bootstrapActivationRequested; @@ -248,10 +308,12 @@ protected override void OnInitialized() { _snapshot = UpdateService.GetSnapshot(); + _runtimeSnapshot = RuntimeWarmup.GetSnapshot(); _includePrerelease = Options.IncludePrerelease; _autoStageWebUpdates = Options.AutoStageWebUpdates; _selectedWebVersion = _snapshot.SelectedWebVersion; UpdateService.Changed += OnUpdatesChanged; + RuntimeWarmup.Changed += OnRuntimeWarmupChanged; if (_snapshot.AppSettingsConflict) _ = LoadAppSettingsConflictAsync(); } @@ -259,6 +321,7 @@ public void Dispose() { UpdateService.Changed -= OnUpdatesChanged; + RuntimeWarmup.Changed -= OnRuntimeWarmupChanged; } private void OnUpdatesChanged() => _ = InvokeAsync(() => @@ -276,6 +339,17 @@ _ = LoadAppSettingsConflictAsync(); }); + private IReadOnlyList ManagedRuntimeRows => _runtimeSnapshot.Components + .Where(component => component.Component is ManagedRuntimeInstallComponent.Magnetar + or ManagedRuntimeInstallComponent.DedicatedServer) + .ToList(); + + private void OnRuntimeWarmupChanged() => _ = InvokeAsync(() => + { + _runtimeSnapshot = RuntimeWarmup.GetSnapshot(); + StateHasChanged(); + }); + private async Task CheckNowAsync() { await RunBusyAsync( @@ -354,6 +428,22 @@ } } + private async Task CheckMagnetarNowAsync() + { + await RunRuntimeBusyAsync( + () => RuntimeWarmup.CheckMagnetarNowAsync(), + "Magnetar update check complete.", + "Magnetar update check failed"); + } + + private async Task CheckDedicatedServerNowAsync() + { + await RunRuntimeBusyAsync( + () => RuntimeWarmup.CheckDedicatedServerNowAsync(), + "Dedicated Server update check complete.", + "Dedicated Server update check failed"); + } + private async Task HandleSelectedWebVersionChanged(string? version) { version ??= string.Empty; @@ -500,6 +590,25 @@ } } + private async Task RunRuntimeBusyAsync(Func action, string successMessage, string failurePrefix) + { + _runtimeBusy = true; + try + { + await action(); + _runtimeSnapshot = RuntimeWarmup.GetSnapshot(); + Snackbar.Add(successMessage, Severity.Success); + } + catch (Exception exception) + { + Snackbar.Add($"{failurePrefix}: {exception.Message}", Severity.Error); + } + finally + { + _runtimeBusy = false; + } + } + private Severity GetStatusSeverity() => _snapshot.Status switch { _ when _snapshot.AppSettingsConflict => Severity.Warning, @@ -512,6 +621,14 @@ private static string FormatTimestamp(DateTimeOffset? timestamp) => timestamp is null ? "never" : timestamp.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); + private static string FormatRuntimeVersion(ManagedRuntimeComponentSnapshot component) + { + if (!string.IsNullOrWhiteSpace(component.Version)) + return component.Version; + + return string.IsNullOrWhiteSpace(component.Path) ? "not installed" : "installed (version unknown)"; + } + private string FormatBootstrapVersion() => string.IsNullOrWhiteSpace(_snapshot.CurrentBootstrapVersion) ? "not managed by Bootstrap" diff --git a/Quasar/Program.cs b/Quasar/Program.cs index 8c4a3a6..e490f82 100644 --- a/Quasar/Program.cs +++ b/Quasar/Program.cs @@ -411,6 +411,13 @@ or DedicatedServerProcessState.Restarting if (authOptions.Enabled) razorComponents.RequireAuthorization(QuasarPolicyNames.CanView); + app.Services.GetRequiredService>().LogInformation( + "Quasar {Version} starting. BootstrapVersion={BootstrapVersion}; HostId={HostId}; DataDirectory={DataDirectory}.", + webServiceOptions.Version, + string.IsNullOrWhiteSpace(webServiceOptions.BootstrapVersion) ? "none" : webServiceOptions.BootstrapVersion, + webServiceOptions.HostId, + MagnetarPaths.GetQuasarDirectory()); + using var gracefulShutdownSignals = RegisterGracefulShutdownSignals(app.Services); app.Run(); } diff --git a/Quasar/Services/AgentSocketHandler.cs b/Quasar/Services/AgentSocketHandler.cs index d1e2662..9737690 100644 --- a/Quasar/Services/AgentSocketHandler.cs +++ b/Quasar/Services/AgentSocketHandler.cs @@ -151,6 +151,26 @@ await _supervisor.SetGoalStateAsync( break; + case WireMessageKind.AdminRestart: + if (_registry.TryGetUniqueName(connectionId, out var restartedUniqueName)) + { + _logger.LogInformation( + "Admin restarted server {UniqueName} in-game; keeping goal state On and tracking restart.", + restartedUniqueName); + + await _supervisor.BeginAdminRestartAsync( + restartedUniqueName, + _lifetime.ApplicationStopping); + } + else + { + _logger.LogWarning( + "Received admin-restart signal for unknown connection {ConnectionId}.", + connectionId); + } + + break; + case WireMessageKind.Ping: await SendAsync(socket, new AgentWireMessage { diff --git a/Quasar/Services/DedicatedServerSupervisor.cs b/Quasar/Services/DedicatedServerSupervisor.cs index 1a6dca3..e07d9b1 100644 --- a/Quasar/Services/DedicatedServerSupervisor.cs +++ b/Quasar/Services/DedicatedServerSupervisor.cs @@ -436,6 +436,43 @@ public async Task KillStartingServerAsync(string uniqueName, CancellationToken c SetStopped(uniqueName, "Start cancelled."); } + public async Task BeginAdminRestartAsync(string uniqueName, CancellationToken cancellationToken = default) + { + var definition = _catalog.GetServer(uniqueName); + if (definition is null) + throw new InvalidOperationException($"Unknown Quasar server '{uniqueName}'."); + + if (definition.GoalState != DedicatedServerGoalState.On || !definition.AutoStart) + { + definition.GoalState = DedicatedServerGoalState.On; + definition.AutoStart = true; + await _catalog.UpsertAsync(definition, cancellationToken); + } + + var changed = false; + var startNow = false; + lock (_sync) + { + if (!_states.TryGetValue(uniqueName, out var state)) + return; + + startNow = !IsProcessActive(state.Process) && !state.StartInProgress; + state.Definition.GoalState = DedicatedServerGoalState.On; + state.Definition.AutoStart = true; + state.StopRequested = false; + state.IsRestartPending = true; + state.State = DedicatedServerProcessState.Restarting; + state.LastMessage = "Restart requested from in-game command."; + changed = true; + } + + if (changed) + NotifyChanged(); + + if (startNow) + await StartServerAsync(uniqueName, cancellationToken); + } + public async Task RestartServerAsync(string uniqueName, CancellationToken cancellationToken = default) { var definition = _catalog.GetServer(uniqueName); @@ -1455,6 +1492,7 @@ private async Task HandleProcessExitedAsync(string uniqueName) DedicatedServerDefinition definition; int exitCode; bool stopRequested; + bool restartRequested; bool shouldRestart = false; int restartDelaySeconds = 0; DateTimeOffset now = DateTimeOffset.UtcNow; @@ -1467,6 +1505,7 @@ private async Task HandleProcessExitedAsync(string uniqueName) definition = Clone(state.Definition); exitCode = SafeGetExitCode(state.Process); stopRequested = state.StopRequested || _isStopping; + restartRequested = state.IsRestartPending && state.State == DedicatedServerProcessState.Restarting; if (state.StartedAtUtc.HasValue && (now - state.StartedAtUtc.Value) >= RestartCounterResetWindow) state.RestartAttempts = 0; @@ -1480,6 +1519,17 @@ private async Task HandleProcessExitedAsync(string uniqueName) ResetHealthTracking(state); if (!stopRequested && + restartRequested && + definition.GoalState == DedicatedServerGoalState.On) + { + state.RestartAttempts = 0; + state.IsRestartPending = true; + state.State = DedicatedServerProcessState.Restarting; + state.LastMessage = $"Restart command exited with code {exitCode}. Starting."; + shouldRestart = true; + restartDelaySeconds = Math.Max(0, definition.RestartDelaySeconds); + } + else if (!stopRequested && definition.GoalState == DedicatedServerGoalState.On && definition.RestartOnCrash) { @@ -2298,7 +2348,13 @@ private static ServerHealthAssessment BuildExistingSimulationAssessment(ManagedS !state.SimulationProgressWindowSeconds.HasValue || !state.SimulationFramesAdvanced.HasValue) { - return new ServerHealthAssessment(DedicatedServerHealthState.Warning, waitingSummary); + var waitingState = string.Equals( + waitingSummary, + "Collecting simulation progress baseline.", + StringComparison.Ordinal) + ? DedicatedServerHealthState.Unknown + : DedicatedServerHealthState.Warning; + return new ServerHealthAssessment(waitingState, waitingSummary); } if (state.SimulationProgressScore.Value < state.Definition.MinimumSimulationProgressScore) diff --git a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs index 51f6a72..79c6e04 100644 --- a/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs +++ b/Quasar/Services/ManagedDedicatedServerRuntimeResolver.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO.Compression; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; @@ -148,7 +149,8 @@ public async Task EnsureManagedRuntimeReadyAsync( ManagedRuntimeInstallComponent.SteamCmd, ManagedRuntimeInstallPhase.Ready, "SteamCMD installed.", - Path: steamCmdPath)); + Path: steamCmdPath, + Version: GetFileVersion(steamCmdPath))); var dedicatedServer64Path = Path.Combine(_options.DedicatedServerInstallDirectory, "DedicatedServer64"); var dedicatedServerReady = IsValidDedicatedServer64Directory(dedicatedServer64Path); @@ -206,12 +208,15 @@ await RunSteamCmdAsync( OperatingSystem.IsLinux() ? "SteamCMD and linux64 native runtime ready." : "SteamCMD ready.", - Path: OperatingSystem.IsLinux() ? steamCmdRuntimePath : steamCmdPath)); + Path: OperatingSystem.IsLinux() ? steamCmdRuntimePath : steamCmdPath, + Version: GetFileVersion(steamCmdPath))); + var dedicatedServerVersion = GetDedicatedServerVersion(dedicatedServer64Path); progress?.Report(new ManagedRuntimeInstallProgress( ManagedRuntimeInstallComponent.DedicatedServer, ManagedRuntimeInstallPhase.Ready, - "Space Engineers Dedicated Server ready.", - Path: dedicatedServer64Path)); + BuildDedicatedServerReadyMessage(dedicatedServerVersion), + Path: dedicatedServer64Path, + Version: dedicatedServerVersion)); await EnsureManagedMagnetarInstallAsync(ManagedServerRuntime.DotNet10, progress, cancellationToken); @@ -228,6 +233,58 @@ public Task EnsureManagedMagnetarCurrentAsync( CancellationToken cancellationToken = default) => EnsureManagedMagnetarInstallAsync(ManagedServerRuntime.DotNet10, progress, cancellationToken); + public async Task EnsureManagedDedicatedServerCurrentAsync( + IProgress? progress = null, + CancellationToken cancellationToken = default) + { + progress?.Report(new ManagedRuntimeInstallProgress( + ManagedRuntimeInstallComponent.SteamCmd, + ManagedRuntimeInstallPhase.Checking, + "Checking SteamCMD install.")); + + var steamCmdPath = await EnsureManagedSteamCmdInstallAsync(progress, cancellationToken); + if (string.IsNullOrWhiteSpace(steamCmdPath)) + throw new InvalidOperationException("SteamCMD is not installed and could not be downloaded."); + + progress?.Report(new ManagedRuntimeInstallProgress( + ManagedRuntimeInstallComponent.SteamCmd, + ManagedRuntimeInstallPhase.Ready, + "SteamCMD installed.", + Path: steamCmdPath, + Version: GetFileVersion(steamCmdPath))); + + var dedicatedServer64Path = await TryEnsureManagedDedicatedServerInstallAsync( + cancellationToken, + steamCmdPath, + progress); + if (!IsValidDedicatedServer64Directory(dedicatedServer64Path)) + throw new InvalidOperationException("Space Engineers Dedicated Server is not installed and could not be updated."); + + var version = GetDedicatedServerVersion(dedicatedServer64Path); + progress?.Report(new ManagedRuntimeInstallProgress( + ManagedRuntimeInstallComponent.DedicatedServer, + ManagedRuntimeInstallPhase.Ready, + BuildDedicatedServerReadyMessage(version), + Path: dedicatedServer64Path, + Version: version)); + } + + public ManagedRuntimeVersionSnapshot GetInstalledVersions() + { + var steamCmdPath = ResolveInstalledSteamCmdPath(); + var magnetarPath = ResolveInstalledMagnetarPath(); + var dedicatedServer64Path = ResolveInstalledDedicatedServer64Path(); + var installedMagnetar = ReadInstalledMagnetarRelease(_options.MagnetarInstallDirectory); + + return new ManagedRuntimeVersionSnapshot( + SteamCmdPath: steamCmdPath, + SteamCmdVersion: GetFileVersion(steamCmdPath), + MagnetarPath: magnetarPath, + MagnetarVersion: installedMagnetar?.DisplayName ?? GetFileVersion(magnetarPath), + DedicatedServer64Path: dedicatedServer64Path, + DedicatedServerVersion: GetDedicatedServerVersion(dedicatedServer64Path)); + } + private Task EnsureManagedMagnetarInstallAsync( ManagedServerRuntime runtime, IProgress? progress, @@ -272,7 +329,8 @@ private async Task EnsureLinuxManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, BuildMagnetarReadyMessage(archive), - Path: binaryLauncherPath)); + Path: binaryLauncherPath, + Version: GetMagnetarVersion(archive))); return binaryLauncherPath; } @@ -321,7 +379,8 @@ private async Task EnsureLinuxManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, BuildMagnetarReadyMessage(archive), - Path: binaryLauncherPath)); + Path: binaryLauncherPath, + Version: GetMagnetarVersion(archive))); return binaryLauncherPath; } catch (Exception exception) when (!cancellationToken.IsCancellationRequested && @@ -337,7 +396,8 @@ private async Task EnsureLinuxManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, "Using existing Magnetar runtime after update failure.", - Path: binaryLauncherPath)); + Path: binaryLauncherPath, + Version: DescribeInstalledMagnetarRelease(installDirectory))); return binaryLauncherPath; } finally @@ -381,7 +441,8 @@ private async Task EnsureWindowsManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, BuildMagnetarReadyMessage(archive), - Path: launcherPath)); + Path: launcherPath, + Version: GetMagnetarVersion(archive))); return launcherPath; } @@ -428,7 +489,8 @@ private async Task EnsureWindowsManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, BuildMagnetarReadyMessage(archive), - Path: launcherPath)); + Path: launcherPath, + Version: GetMagnetarVersion(archive))); return launcherPath; } catch (Exception exception) when (!cancellationToken.IsCancellationRequested && File.Exists(launcherPath)) @@ -442,7 +504,8 @@ private async Task EnsureWindowsManagedMagnetarInstallAsync( ManagedRuntimeInstallComponent.Magnetar, ManagedRuntimeInstallPhase.Ready, "Using existing Magnetar runtime after update failure.", - Path: launcherPath)); + Path: launcherPath, + Version: DescribeInstalledMagnetarRelease(installDirectory))); return launcherPath; } finally @@ -638,6 +701,104 @@ private static string BuildMagnetarReadyMessage(MagnetarArchiveReference archive ? "Magnetar runtime ready." : $"Magnetar runtime {archive.DisplayName} ready."; + private static string GetMagnetarVersion(MagnetarArchiveReference archive) => + archive.SourceKind == MagnetarArchiveSourceKinds.ExistingUnknown + ? string.Empty + : archive.DisplayName; + + private static string BuildDedicatedServerReadyMessage(string version) => + string.IsNullOrWhiteSpace(version) + ? "Space Engineers Dedicated Server ready." + : $"Space Engineers Dedicated Server {version} ready."; + + private string ResolveInstalledSteamCmdPath() + { + if (!string.IsNullOrWhiteSpace(_options.SteamCmdPath) && File.Exists(_options.SteamCmdPath)) + return _options.SteamCmdPath; + + var managedPath = FindSteamCmdExecutable(_options.SteamCmdInstallDirectory); + if (!string.IsNullOrWhiteSpace(managedPath)) + return managedPath; + + return ResolveSteamCmdPathFromEnvironment(); + } + + private string ResolveInstalledMagnetarPath() + { + if (OperatingSystem.IsWindows()) + { + var interimPath = Path.Combine(_options.MagnetarInstallDirectory, GetWindowsMagnetarLauncherFileName(ManagedServerRuntime.DotNet10)); + if (File.Exists(interimPath)) + return interimPath; + + var legacyPath = Path.Combine(_options.MagnetarInstallDirectory, GetWindowsMagnetarLauncherFileName(ManagedServerRuntime.NetFramework48)); + return File.Exists(legacyPath) ? legacyPath : string.Empty; + } + + return FindImmediateFile(Path.Combine(_options.MagnetarInstallDirectory, "Bin"), MagnetarLauncherFileNames) ?? string.Empty; + } + + private string ResolveInstalledDedicatedServer64Path() + { + if (IsValidDedicatedServer64Directory(_options.DedicatedServer64OverridePath)) + return _options.DedicatedServer64OverridePath; + + var managedPath = Path.Combine(_options.DedicatedServerInstallDirectory, "DedicatedServer64"); + if (IsValidDedicatedServer64Directory(managedPath)) + return managedPath; + + return EnumerateDedicatedServer64Candidates().FirstOrDefault(IsValidDedicatedServer64Directory) ?? string.Empty; + } + + private static string GetDedicatedServerVersion(string dedicatedServer64Path) + { + if (!IsValidDedicatedServer64Directory(dedicatedServer64Path)) + return string.Empty; + + return FirstNonEmpty( + GetFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName + ".exe")), + GetFileVersion(Path.Combine(dedicatedServer64Path, DedicatedServerExecutableName)), + GetFileVersion(Path.Combine(dedicatedServer64Path, "SpaceEngineers.Game.dll")), + GetFileVersion(Path.Combine(dedicatedServer64Path, "Sandbox.Game.dll"))); + } + + private static string GetFileVersion(string path) + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + return string.Empty; + + try + { + var version = FileVersionInfo.GetVersionInfo(path); + var fileVersion = FirstNonEmpty(version.ProductVersion, version.FileVersion); + if (!string.IsNullOrWhiteSpace(fileVersion)) + return fileVersion; + } + catch + { + } + + try + { + return AssemblyName.GetAssemblyName(path).Version?.ToString() ?? string.Empty; + } + catch + { + return string.Empty; + } + } + + private static string FirstNonEmpty(params string?[] values) + { + foreach (var value in values) + { + if (!string.IsNullOrWhiteSpace(value)) + return value; + } + + return string.Empty; + } + private static InstalledMagnetarRelease? ReadInstalledMagnetarRelease(string installDirectory) { var markerPath = GetMagnetarReleaseMarkerPath(installDirectory); @@ -1675,12 +1836,21 @@ public static ManagedRuntimeReadiness Failed( new(false, steamCmdPath, steamCmdRuntimePath, dedicatedServer64Path, failureMessage); } +public sealed record ManagedRuntimeVersionSnapshot( + string SteamCmdPath, + string SteamCmdVersion, + string MagnetarPath, + string MagnetarVersion, + string DedicatedServer64Path, + string DedicatedServerVersion); + public sealed record ManagedRuntimeInstallProgress( ManagedRuntimeInstallComponent Component, ManagedRuntimeInstallPhase Phase, string Message, int? Percent = null, - string Path = ""); + string Path = "", + string Version = ""); public enum ManagedRuntimeInstallComponent { diff --git a/Quasar/Services/ManagedRuntimeWarmupService.cs b/Quasar/Services/ManagedRuntimeWarmupService.cs index c8b99d9..b458268 100644 --- a/Quasar/Services/ManagedRuntimeWarmupService.cs +++ b/Quasar/Services/ManagedRuntimeWarmupService.cs @@ -4,7 +4,7 @@ namespace Quasar.Services; public sealed class ManagedRuntimeWarmupService : BackgroundService { - private static readonly TimeSpan MagnetarUpdateCheckInterval = TimeSpan.FromHours(1); + public static readonly TimeSpan MagnetarUpdateCheckPeriod = TimeSpan.FromHours(1); private readonly ManagedDedicatedServerRuntimeResolver _runtimeResolver; private readonly ILogger _logger; private readonly object _sync = new(); @@ -25,7 +25,7 @@ public ManagedRuntimeWarmupSnapshot GetSnapshot() { lock (_sync) { - return _snapshot.Copy(); + return ApplyInstalledVersions(_snapshot.Copy(), _runtimeResolver.GetInstalledVersions()); } } @@ -56,11 +56,17 @@ public string BlockLaunchMessage public Task RetryAsync(CancellationToken cancellationToken = default) => RunWarmupAsync(cancellationToken); + public Task CheckMagnetarNowAsync(CancellationToken cancellationToken = default) => + RunMagnetarUpdateCheckAsync(cancellationToken); + + public Task CheckDedicatedServerNowAsync(CancellationToken cancellationToken = default) => + RunDedicatedServerUpdateCheckAsync(cancellationToken); + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await RunWarmupAsync(stoppingToken); - using var timer = new PeriodicTimer(MagnetarUpdateCheckInterval); + using var timer = new PeriodicTimer(MagnetarUpdateCheckPeriod); try { while (await timer.WaitForNextTickAsync(stoppingToken)) @@ -133,6 +139,33 @@ private async Task RunMagnetarUpdateCheckAsync(CancellationToken stoppingToken) } } + private async Task RunDedicatedServerUpdateCheckAsync(CancellationToken stoppingToken) + { + if (!await _runLock.WaitAsync(0, stoppingToken)) + return; + + try + { + var progress = new Progress(ApplyProgress); + await _runtimeResolver.EnsureManagedDedicatedServerCurrentAsync(progress, stoppingToken); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _logger.LogWarning(exception, "Managed Dedicated Server update check failed."); + ApplyProgress(new ManagedRuntimeInstallProgress( + ManagedRuntimeInstallComponent.DedicatedServer, + ManagedRuntimeInstallPhase.Failed, + exception.Message)); + } + finally + { + _runLock.Release(); + } + } + private void SetState(ManagedRuntimeWarmupState state, string message) { lock (_sync) @@ -175,6 +208,12 @@ private void ApplyProgress(ManagedRuntimeInstallProgress progress) Message = progress.Message, Percent = progress.Percent, Path = progress.Path, + Version = string.IsNullOrWhiteSpace(progress.Version) ? component.Version : progress.Version, + LastCheckedUtc = progress.Phase is ManagedRuntimeInstallPhase.Checking + or ManagedRuntimeInstallPhase.Ready + or ManagedRuntimeInstallPhase.Failed + ? DateTimeOffset.UtcNow + : component.LastCheckedUtc, }) with { Message = state is ManagedRuntimeComponentState.Checking @@ -189,6 +228,41 @@ or ManagedRuntimeComponentState.Installing Changed?.Invoke(); } + private static ManagedRuntimeWarmupSnapshot ApplyInstalledVersions( + ManagedRuntimeWarmupSnapshot snapshot, + ManagedRuntimeVersionSnapshot versions) => + snapshot.WithComponents(component => component.Component switch + { + ManagedRuntimeInstallComponent.SteamCmd => ApplyInstalledVersion( + component, + versions.SteamCmdPath, + versions.SteamCmdVersion), + ManagedRuntimeInstallComponent.Magnetar => ApplyInstalledVersion( + component, + versions.MagnetarPath, + versions.MagnetarVersion), + ManagedRuntimeInstallComponent.DedicatedServer => ApplyInstalledVersion( + component, + versions.DedicatedServer64Path, + versions.DedicatedServerVersion), + _ => component, + }); + + private static ManagedRuntimeComponentSnapshot ApplyInstalledVersion( + ManagedRuntimeComponentSnapshot component, + string path, + string version) + { + if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(version)) + return component; + + return component with + { + Path = string.IsNullOrWhiteSpace(component.Path) ? path : component.Path, + Version = string.IsNullOrWhiteSpace(component.Version) ? version : component.Version, + }; + } + private static ManagedRuntimeComponentState MapState(ManagedRuntimeInstallPhase phase) => phase switch { ManagedRuntimeInstallPhase.Checking => ManagedRuntimeComponentState.Checking, @@ -289,4 +363,8 @@ public sealed record ManagedRuntimeComponentSnapshot public int? Percent { get; init; } public string Path { get; init; } = string.Empty; + + public string Version { get; init; } = string.Empty; + + public DateTimeOffset? LastCheckedUtc { get; init; } }