Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Docs/BuildingAndDevelopment.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ The solution file is `Quasar.sln`.
generated profile beside `Quasar.Agent.dll`. Viewer scene data is requested
through `IQuasarCompanionChannel` from the viewer's Magnetar companion plugin;
Quasar core does not carry viewer scene DTOs or a viewer-specific HTTP API.
Runtime-only packaged installs can run Quasar, but QuasarHub source-built UI
plugin install/update requires a matching .NET SDK on `PATH`; the UI disables
those build actions when the SDK preflight fails.

## Managed runtime selection

Expand Down
12 changes: 12 additions & 0 deletions Docs/LinuxDeploymentAndUpdates.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ first adds Microsoft's Debian 13 package feed with
`packages-microsoft-prod.deb`, then runs `apt-get update` and installs the
selected .NET package.

Packaged installs can run Quasar with only the runtime, but QuasarHub UI plugin
install/update builds source projects with `dotnet build` and requires the .NET
10 SDK. `install.sh` offers that SDK as an optional step for package/no-build
installs; pass `--install-ui-plugin-sdk` to request it explicitly. If the SDK is
not installed, `/settings/ui-plugins` disables install/update actions and shows
`.NET SDK required to build UI plugins`. On Linux, that warning can also invoke
`install.sh --install-ui-plugin-sdk-only --yes` from the UI. The action only
installs the SDK and exits; it does not rewrite the service or reinstall Quasar.
It still uses the system package manager, so it can fail under a service account
unless `sudo -n` can install packages without a password or Quasar is already
running with sufficient privileges.

```bash
mkdir -p ~/.local/share/Quasar
tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1
Expand Down
11 changes: 11 additions & 0 deletions Docs/QuasarPluginSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ The `/settings/ui-plugins` page now manages the QuasarHub catalog:
- shows installed, update-available, hidden, and invalid package states
- enables or disables installed packages for the next restart
- clones/fetches the plugin repository and checks out the pinned commit
- checks for the matching .NET SDK before building source packages
- builds the declared plugin project with `dotnet build`
- passes `QuasarPluginAbstractionsAssembly` to the build so the plugin compiles
against the contract DLL loaded by the running Quasar worker
Expand Down Expand Up @@ -192,6 +193,16 @@ The installer passes the running worker's physical
`QuasarPluginAbstractionsAssembly`. Release packaging keeps that DLL beside the
single-file worker so Bootstrap-managed installs can build UI plugins from
QuasarHub without needing a NuGet package.
The web worker must still have a matching .NET SDK on `PATH`; the ASP.NET Core
runtime is enough to run Quasar, but not enough to compile source-built UI
plugins. At startup Quasar checks `dotnet --list-sdks`; if the required SDK is
missing, `/settings/ui-plugins` shows `.NET SDK required to build UI plugins`
and disables QuasarHub install/update buttons until the SDK is installed and the
check is refreshed. On Linux, the warning includes an **Install SDK** action that
runs `install.sh --install-ui-plugin-sdk-only --yes`, captures the installer
output, refreshes the SDK preflight, and leaves Quasar service files untouched.
This can still fail if package installation requires an interactive sudo
password.
Owned companion build output is written under
`{installed package}/.quasar/companions/{companion id}`. Quasar passes
`MagnetarProtocolAssembly` from the running worker or staged `Agent/` directory
Expand Down
8 changes: 8 additions & 0 deletions Docs/QuickStart.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ Declining the prompt exits before files or services are changed.
On Debian 13, the prompt also includes the Microsoft package feed bootstrap
commands needed before installing the .NET packages.

Packaged installs need only the runtime to run Quasar. QuasarHub UI plugin
install/update compiles source with `dotnet build`, so install the .NET 10 SDK
too by accepting the optional prompt or passing `--install-ui-plugin-sdk` when
you want source-built UI plugins from QuasarHub. On Linux, the UI Plugins page
can also run the install script's SDK-only path when the SDK is missing.

**Linux — systemd**

```bash
mkdir -p ~/.local/share/Quasar
tar -xzf quasar-installer-linux.tar.gz -C ~/.local/share/Quasar --strip-components=1
~/.local/share/Quasar/install.sh --start # installs in place and starts quasar.service
# Optional SDK for QuasarHub source-built UI plugin installs:
# ~/.local/share/Quasar/install.sh --start --install-ui-plugin-sdk
```

The Linux installer defaults to a user systemd service, uses the extracted
Expand Down
6 changes: 6 additions & 0 deletions Docs/WindowsDeploymentAndUpdates.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ systemd to restart it — that path is unchanged.
Install the **.NET 10 runtime** before running `install.ps1`. The installer checks
for `Microsoft.NETCore.App` 10.x before staging files, publishing, or registering
the Scheduled Task, and exits with install instructions if it is missing.
The runtime is enough to run packaged Quasar. QuasarHub UI plugin install/update
compiles source with `dotnet build` and requires the **.NET 10 SDK**; when the SDK
is missing, `/settings/ui-plugins` shows `.NET SDK required to build UI plugins`
and disables install/update buttons. Automatic SDK install from the UI is a
Linux `install.sh` feature; Windows operators should install the .NET SDK
manually and refresh the SDK check.

```powershell
# From an extracted quasar-installer-windows.zip, in an elevated PowerShell:
Expand Down
188 changes: 180 additions & 8 deletions Quasar/Components/Pages/UiPlugins.razor
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
Variant="Variant.Outlined">
@(PluginCatalog.SafeMode ? "Safe boot active" : "Dynamic plugins enabled")
</MudChip>
<MudChip T="string"
Color="@GetDotNetSdkChipColor()"
Variant="Variant.Outlined">
@GetDotNetSdkChipText()
</MudChip>
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.RestartAlt"
Expand All @@ -36,6 +41,56 @@
</MudStack>
</MudStack>

@if (!CanBuildUiPlugins)
{
var sdkInstallAvailability = HubCatalog.GetDotNetSdkInstallAvailability();
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
<MudStack Spacing="2">
<MudStack Row="true" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="2">
<span><strong>.NET SDK required to build UI plugins.</strong> @_dotNetSdkStatus.Message Install the .NET SDK to install or update source-built Quasar UI plugins.</span>
<MudStack Row="true" Wrap="Wrap.Wrap" Spacing="1" AlignItems="AlignItems.Center">
<MudTooltip Text="@sdkInstallAvailability.Message" Disabled="@sdkInstallAvailability.CanInstall">
<span>
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Download"
Disabled="@(!sdkInstallAvailability.CanInstall || DotNetSdkInstallerBusy)"
OnClick="InstallDotNetSdkAsync">
@(DotNetSdkInstallerBusy ? "Installing SDK..." : "Install SDK")
</MudButton>
</span>
</MudTooltip>
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Refresh"
Disabled="@DotNetSdkInstallerBusy"
OnClick="RefreshDotNetSdkStatus">
Refresh SDK check
</MudButton>
</MudStack>
</MudStack>
@if (_lastDotNetSdkInstallResult is not null)
{
<MudAlert Severity="@(_lastDotNetSdkInstallResult.Succeeded ? Severity.Success : Severity.Error)" Variant="Variant.Outlined" Dense="true">
<MudStack Spacing="1">
<MudText Typo="Typo.body2">@_lastDotNetSdkInstallResult.Message</MudText>
@if (!string.IsNullOrWhiteSpace(_lastDotNetSdkInstallResult.Output))
{
<MudExpansionPanels MultiExpansion="false" Elevation="0" Dense="true">
<MudExpansionPanel Text="Installer output">
<MudText Typo="Typo.caption" Class="mono" Style="white-space: pre-wrap; max-height: 18rem; overflow: auto; display: block;">@TrimInstallerOutput(_lastDotNetSdkInstallResult.Output)</MudText>
</MudExpansionPanel>
</MudExpansionPanels>
}
</MudStack>
</MudAlert>
}
</MudStack>
</MudAlert>
}

<MudGrid>
<MudItem xs="12" md="3">
<MudPaper Class="pa-4" Style="height:100%">
Expand Down Expand Up @@ -189,6 +244,7 @@
<RowTemplate>
@{
var installState = HubCatalog.GetInstallState(context);
var installDisabledReason = GetHubInstallDisabledReason(context);
}
<MudTd DataLabel="Status">
<MudStack Spacing="0">
Expand Down Expand Up @@ -231,14 +287,18 @@
</MudTd>
<MudTd DataLabel="" Class="quasar-actions-column">
<div class="ui-plugin-actions">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@GetHubInstallButtonIcon(context, installState)"
Disabled="@IsHubInstallDisabled(context)"
OnClick="@(() => InstallOrUpdateHubPluginAsync(context))">
@GetHubInstallButtonText(context, installState)
</MudButton>
<MudTooltip Text="@installDisabledReason" Disabled="@string.IsNullOrWhiteSpace(installDisabledReason)">
<span>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@GetHubInstallButtonIcon(context, installState)"
Disabled="@IsHubInstallDisabled(context)"
OnClick="@(() => InstallOrUpdateHubPluginAsync(context))">
@GetHubInstallButtonText(context, installState)
</MudButton>
</span>
</MudTooltip>
@if (installState.Installed)
{
<MudButton Variant="Variant.Outlined"
Expand Down Expand Up @@ -474,7 +534,15 @@
private string _hubSearch = string.Empty;
private bool _hubRefreshing;
private bool _restartRequested;
private bool _dotNetSdkInstallerBusy;
private string _hubBusyCatalogId = string.Empty;
private QuasarDotNetSdkInstallResult? _lastDotNetSdkInstallResult;
private QuasarDotNetSdkStatus _dotNetSdkStatus = new(
Environment.Version.Major,
DotNetOnPath: false,
RequiredSdkAvailable: false,
[],
"SDK check pending.");

private string SafeModeMarkerPath => QuasarUiPluginCatalog.SafeModeMarkerPath;

Expand All @@ -488,6 +556,10 @@

private bool HubOperationInProgress => !string.IsNullOrWhiteSpace(_hubBusyCatalogId);

private bool CanBuildUiPlugins => _dotNetSdkStatus.CanBuildUiPlugins;

private bool DotNetSdkInstallerBusy => _dotNetSdkInstallerBusy || HubCatalog.DotNetSdkInstallInProgress;

private IReadOnlyDictionary<string, object?> PluginsPageParameters => new Dictionary<string, object?>
{
["UiPlugins"] = PluginCatalog,
Expand All @@ -499,6 +571,7 @@

protected override void OnInitialized()
{
_dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
HubCatalog.Changed += HandleHubCatalogChanged;
PluginStates.Changed += HandlePluginStatesChanged;
}
Expand Down Expand Up @@ -591,6 +664,9 @@
if (HubOperationInProgress)
return;

if (!EnsureDotNetSdkForPluginBuild())
return;

_hubBusyCatalogId = entry.CatalogId;
try
{
Expand All @@ -608,6 +684,50 @@
}
}

private async Task InstallDotNetSdkAsync()
{
if (DotNetSdkInstallerBusy)
return;

var availability = HubCatalog.GetDotNetSdkInstallAvailability();
if (!availability.CanInstall)
{
Snackbar.Add(availability.Message, Severity.Warning);
return;
}

var confirmed = await DialogService.ShowMessageBoxAsync(
"Install .NET SDK?",
"Quasar will run install.sh to install the .NET SDK required for source-built UI plugins. This may install system packages and can fail if sudo needs a password.",
yesText: "Install SDK",
cancelText: "Cancel");
if (confirmed != true)
return;

_dotNetSdkInstallerBusy = true;
_lastDotNetSdkInstallResult = null;
try
{
var result = await HubCatalog.InstallDotNetSdkAsync();
_lastDotNetSdkInstallResult = result;
_dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
Snackbar.Add(result.Message, result.Succeeded ? Severity.Success : Severity.Error);
}
catch (Exception exception)
{
_lastDotNetSdkInstallResult = new QuasarDotNetSdkInstallResult(
Succeeded: false,
exception.Message,
string.Empty);
Snackbar.Add(exception.Message, Severity.Error);
}
finally
{
_dotNetSdkInstallerBusy = false;
await InvokeAsync(StateHasChanged);
}
}

private async Task RemoveHubPluginAsync(QuasarUiPluginHubEntry entry)
{
if (HubOperationInProgress)
Expand Down Expand Up @@ -710,11 +830,32 @@
string.Equals(_hubBusyCatalogId, entry.CatalogId, StringComparison.OrdinalIgnoreCase);

private bool IsHubInstallDisabled(QuasarUiPluginHubEntry entry) =>
!CanBuildUiPlugins ||
HubOperationInProgress ||
_hubRefreshing ||
string.IsNullOrWhiteSpace(entry.RepoId) ||
string.IsNullOrWhiteSpace(entry.Commit);

private string GetHubInstallDisabledReason(QuasarUiPluginHubEntry entry)
{
if (!CanBuildUiPlugins)
return $".NET SDK required to build UI plugins. Install .NET {_dotNetSdkStatus.RequiredMajorVersion} SDK.";

if (HubOperationInProgress)
return "Another QuasarHub operation is running.";

if (_hubRefreshing)
return "QuasarHub catalog refresh is running.";

if (string.IsNullOrWhiteSpace(entry.RepoId))
return "QuasarHub entry has no repository.";

if (string.IsNullOrWhiteSpace(entry.Commit))
return "QuasarHub entry has no pinned commit.";

return string.Empty;
}

private string GetHubInstallButtonText(QuasarUiPluginHubEntry entry, QuasarUiPluginInstallState installState)
{
if (IsHubBusy(entry))
Expand Down Expand Up @@ -786,6 +927,36 @@
private static string GetHubEnableButtonIcon(QuasarUiPluginInstallState installState) =>
installState.Enabled ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility;

private Color GetDotNetSdkChipColor() =>
CanBuildUiPlugins ? Color.Success : Color.Warning;

private string GetDotNetSdkChipText() =>
CanBuildUiPlugins ? $".NET {_dotNetSdkStatus.RequiredMajorVersion} SDK ready" : ".NET SDK missing";

private void RefreshDotNetSdkStatus()
{
_dotNetSdkStatus = HubCatalog.RefreshDotNetSdkStatus();
Snackbar.Add(_dotNetSdkStatus.Message, CanBuildUiPlugins ? Severity.Success : Severity.Warning);
}

private bool EnsureDotNetSdkForPluginBuild()
{
_dotNetSdkStatus = HubCatalog.RefreshDotNetSdkStatus();
if (CanBuildUiPlugins)
return true;

Snackbar.Add(_dotNetSdkStatus.Message, Severity.Warning);
return false;
}

private static string TrimInstallerOutput(string output)
{
const int maxCharacters = 8000;
return output.Length <= maxCharacters
? output
: output[^maxCharacters..];
}

private static string GetHubDisplayName(QuasarUiPluginHubEntry entry) =>
string.IsNullOrWhiteSpace(entry.FriendlyName) ? entry.CatalogId : entry.FriendlyName;

Expand Down Expand Up @@ -839,6 +1010,7 @@

private void HandleHubCatalogChanged()
{
_dotNetSdkStatus = HubCatalog.GetDotNetSdkStatus();
_ = InvokeAsync(StateHasChanged);
}

Expand Down
Loading
Loading