Merge pull request 'perf(welcome): cut first-boot cold-start + add loading affordance' (#20) from fix/welcome-cold-start into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 21s
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 21s
Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
@@ -118,11 +118,19 @@
|
||||
render when launched as a bare Shell Launcher shell). Configure-Kiosk.ps1
|
||||
bakes the silent-elevation UAC policy + the lockdown (Keyboard Filter,
|
||||
DisableTaskMgr, hidden taskbar); the wizard runs fullscreen-topmost on top.
|
||||
|
||||
Launch is via a single hidden-window PowerShell (no `cmd /c` wrapper): the
|
||||
old `cmd /c powershell ...` spawned an extra process AND flashed a visible
|
||||
console window on the bare first-boot desktop — which itself read as "the
|
||||
machine is doing something broken" before the wizard appeared. `-WindowStyle
|
||||
Hidden` + dropping the cmd shim removes that flash and one process off the
|
||||
critical path. Elevation (-Verb RunAs) is still required for ApplyService
|
||||
(account/BitLocker/hardening) and is silent thanks to the baked UAC policy.
|
||||
-->
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<Order>1</Order>
|
||||
<CommandLine>cmd /c powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs"</CommandLine>
|
||||
<CommandLine>powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "Start-Process -FilePath 'C:\Program Files\SilverOS\Welcome\SilverOS.Welcome.App.exe' -Verb RunAs"</CommandLine>
|
||||
<Description>Launch SilverOS Welcome elevated</Description>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
|
||||
@@ -183,6 +183,22 @@ function Copy-WelcomePayload {
|
||||
} else {
|
||||
Write-Warning " No apps dir found at $appsDir -- image will ship with no app catalog."
|
||||
}
|
||||
# Stage the fixed-version WebView2 runtime, if vendored, next to the app.
|
||||
# Cold-start + air-gap: a fixed-version runtime is just files (no installer
|
||||
# step at first boot) and removes the dependency on whether IoT Enterprise LTSC
|
||||
# ships WebView2 at all. Operator populates windows\welcome\runtime\webview2\
|
||||
# with an EXTRACTED "Microsoft Edge WebView2 Fixed Version" distribution (the
|
||||
# folder that contains msedgewebview2.exe) -- handled like the drivers dir:
|
||||
# absent is allowed (VM/dev test), in which case the app falls back to Evergreen.
|
||||
$wv2Src = Join-Path $WindowsDir 'welcome\runtime\webview2'
|
||||
if (Test-Path (Join-Path $wv2Src 'msedgewebview2.exe')) {
|
||||
$wv2Dest = Join-Path $dest 'webview2'
|
||||
$null = New-Item -ItemType Directory -Force $wv2Dest
|
||||
Copy-Item (Join-Path $wv2Src '*') $wv2Dest -Recurse -Force
|
||||
Write-Host " Staged fixed-version WebView2 runtime to $wv2Dest"
|
||||
} else {
|
||||
Write-Warning " No fixed-version WebView2 runtime at $wv2Src (expected msedgewebview2.exe) -- image will rely on the Evergreen runtime being present at first boot. See windows\welcome\runtime\webview2\README.md."
|
||||
}
|
||||
# --- Guard: verify the payload actually landed in the mounted image -------
|
||||
$stagedExe = Join-Path $dest 'SilverOS.Welcome.App.exe'
|
||||
if (-not (Test-Path $stagedExe)) {
|
||||
|
||||
44
windows/welcome/runtime/webview2/README.md
Normal file
44
windows/welcome/runtime/webview2/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Fixed-version WebView2 runtime (vendored)
|
||||
|
||||
The SilverOS Welcome wizard is a MAUI Blazor Hybrid app — it needs the **Microsoft
|
||||
Edge WebView2 Runtime**. IoT Enterprise LTSC images frequently ship **without** it,
|
||||
and even when present, the Evergreen runtime adds first-boot cold-start cost (registry
|
||||
probe; on-demand install if absent). To make first boot fast *and* air-gapped, we bake
|
||||
a **fixed-version** runtime here and point the app at it via
|
||||
`WEBVIEW2_BROWSER_EXECUTABLE_FOLDER` (see `MauiProgram.cs`).
|
||||
|
||||
## What goes in this folder
|
||||
|
||||
The **extracted** contents of a "Microsoft Edge WebView2 Fixed Version" distribution —
|
||||
i.e. this directory must directly contain `msedgewebview2.exe` (plus its sibling DLLs,
|
||||
`*.pak`, locales, etc.). The build (`windows/installer/build.ps1`,
|
||||
`Copy-WelcomePayload`) detects `msedgewebview2.exe` and copies the whole folder to
|
||||
`C:\Program Files\SilverOS\Welcome\webview2\` inside the image.
|
||||
|
||||
```
|
||||
windows/welcome/runtime/webview2/
|
||||
├── README.md <- this file (the only thing committed)
|
||||
├── msedgewebview2.exe <- you add these
|
||||
├── *.dll
|
||||
├── *.pak
|
||||
└── ...
|
||||
```
|
||||
|
||||
## How to obtain it
|
||||
|
||||
1. Download the **Fixed Version** (x64) CAB from the official WebView2 distribution
|
||||
page: <https://developer.microsoft.com/microsoft-edge/webview2/> → "Fixed Version".
|
||||
Match the channel/arch to the target (x64, since the app publishes `win-x64`).
|
||||
2. Expand the CAB and copy the inner runtime folder's contents here so that
|
||||
`msedgewebview2.exe` sits directly in this directory.
|
||||
3. Pin the version in `windows/installer/inputs.manifest.json` alongside the other
|
||||
baked inputs (SBOM hygiene).
|
||||
|
||||
## If you skip this
|
||||
|
||||
The build does **not** fail — it logs a warning and the image relies on whatever
|
||||
Evergreen runtime is present at first boot. Fine for a quick VM smoke test; **not**
|
||||
recommended for shipped LTSC media (risk of a blank/hung wizard and slower cold start).
|
||||
|
||||
> The runtime binaries are **not** committed (large, Microsoft-redistributable, version-
|
||||
> pinned per build). Only this README is tracked.
|
||||
@@ -5,6 +5,16 @@
|
||||
xmlns:components="clr-namespace:SilverOS.Welcome.App.Components;assembly=SilverOS.Welcome.UI"
|
||||
x:Class="SilverOS.Welcome.App.MainPage">
|
||||
|
||||
<!--
|
||||
A native MAUI splash sits ON TOP of the BlazorWebView. MAUI controls render
|
||||
immediately when the window is shown — they do NOT wait on WebView2/.NET JIT —
|
||||
so the user sees branded "loading" within the first frame instead of a blank
|
||||
window for the seconds it takes WebView2 to cold-start and Blazor to boot.
|
||||
The overlay is dismissed in MainPage.xaml.cs once WV2 finishes its first
|
||||
navigation (the index.html splash then carries the eye through Blazor's boot).
|
||||
-->
|
||||
<Grid BackgroundColor="#0b0f14">
|
||||
|
||||
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html"
|
||||
BlazorWebViewInitialized="OnBlazorInitialized"
|
||||
UrlLoading="OnUrlLoading">
|
||||
@@ -13,4 +23,36 @@
|
||||
</BlazorWebView.RootComponents>
|
||||
</BlazorWebView>
|
||||
|
||||
<Grid x:Name="SplashOverlay"
|
||||
BackgroundColor="#0b0f14"
|
||||
InputTransparent="False">
|
||||
<VerticalStackLayout HorizontalOptions="Center"
|
||||
VerticalOptions="Center"
|
||||
Spacing="22">
|
||||
<Label Text="SilverOS"
|
||||
HorizontalOptions="Center"
|
||||
FontFamily="OpenSansRegular"
|
||||
FontSize="42"
|
||||
FontAutoScalingEnabled="False"
|
||||
TextColor="#e8edf5" />
|
||||
<Label Text="WELCOME"
|
||||
HorizontalOptions="Center"
|
||||
FontSize="13"
|
||||
CharacterSpacing="8"
|
||||
TextColor="#00d4ff" />
|
||||
<ActivityIndicator IsRunning="True"
|
||||
Color="#00d4ff"
|
||||
HeightRequest="34"
|
||||
WidthRequest="34"
|
||||
HorizontalOptions="Center"
|
||||
Margin="0,10,0,0" />
|
||||
<Label Text="Preparing your setup…"
|
||||
HorizontalOptions="Center"
|
||||
FontSize="14"
|
||||
TextColor="#8fa4bc" />
|
||||
</VerticalStackLayout>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
</ContentPage>
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace SilverOS.Welcome.App;
|
||||
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
bool _splashDismissed;
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -20,7 +22,13 @@ public partial class MainPage : ContentPage
|
||||
{
|
||||
var wv = e.WebView; // Microsoft.UI.Xaml.Controls.WebView2
|
||||
wv.NavigationCompleted += (a, b) =>
|
||||
{
|
||||
Diag.Log($"WV2 NavigationCompleted ok={b.IsSuccess} status={b.WebErrorStatus}");
|
||||
// First completed navigation = the WebView has content on screen.
|
||||
// Drop the native splash so the (visually identical) in-page splash
|
||||
// carries through Blazor's final boot without a flash of blank.
|
||||
if (b.IsSuccess) DismissSplash();
|
||||
};
|
||||
if (wv.CoreWebView2 is not null)
|
||||
wv.CoreWebView2.ProcessFailed += (a, b) =>
|
||||
Diag.Log("WV2 ProcessFailed: " + b.ProcessFailedKind);
|
||||
@@ -29,6 +37,26 @@ public partial class MainPage : ContentPage
|
||||
#endif
|
||||
}
|
||||
|
||||
// Fade the native splash out once, then collapse it so it never intercepts input.
|
||||
void DismissSplash()
|
||||
{
|
||||
if (_splashDismissed) return;
|
||||
_splashDismissed = true;
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await SplashOverlay.FadeTo(0, 250, Easing.CubicOut);
|
||||
}
|
||||
catch { /* fade is cosmetic — never block on it */ }
|
||||
finally
|
||||
{
|
||||
SplashOverlay.IsVisible = false;
|
||||
SplashOverlay.InputTransparent = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void OnUrlLoading(object? sender, UrlLoadingEventArgs e)
|
||||
=> Diag.Log("UrlLoading: " + e.Url);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,23 @@ public static class MauiProgram
|
||||
Directory.CreateDirectory(wv2);
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", wv2);
|
||||
|
||||
// Cold-start + air-gap: prefer a fixed-version WebView2 runtime baked next to
|
||||
// the app (build.ps1 stages it under .\webview2). This removes two first-boot
|
||||
// costs at once: (1) the Evergreen-runtime registry probe, and (2) the risk
|
||||
// that IoT Enterprise LTSC ships WITHOUT WebView2 entirely — in which case the
|
||||
// wizard would block/blank waiting on an on-demand install. If the baked folder
|
||||
// is absent (VM/dev test), fall through to whatever Evergreen runtime exists.
|
||||
var fixedRuntime = Path.Combine(AppContext.BaseDirectory, "webview2");
|
||||
if (File.Exists(Path.Combine(fixedRuntime, "msedgewebview2.exe")))
|
||||
{
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_BROWSER_EXECUTABLE_FOLDER", fixedRuntime);
|
||||
Diag.Log("WebView2: using baked fixed-version runtime at " + fixedRuntime);
|
||||
}
|
||||
else
|
||||
{
|
||||
Diag.Log("WebView2: no baked runtime found; relying on installed Evergreen");
|
||||
}
|
||||
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
|
||||
@@ -25,6 +25,16 @@
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
|
||||
|
||||
<!--
|
||||
Cold-start: precompile to ReadyToRun so the self-contained image does not
|
||||
JIT the whole app on first launch (this wizard runs exactly once, on the
|
||||
slowest possible "fresh OS, cold disk" path, so first-run JIT is pure cost).
|
||||
R2R only — trimming/NativeAOT are NOT safe for MAUI Blazor Hybrid (heavy
|
||||
reflection in Blazor + DI). Larger binaries are fine: the payload is baked
|
||||
into the image, never downloaded. Only takes effect on `dotnet publish -r win-x64`.
|
||||
-->
|
||||
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -303,6 +303,58 @@ h1:focus { outline: none; }
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ── Boot splash (pre-Blazor; mirrors the native MAUI splash) ───────── */
|
||||
.sm-boot {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.4rem;
|
||||
background: var(--clr-void);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.sm-boot-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 2.6rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--clr-text-hi);
|
||||
}
|
||||
|
||||
.sm-boot-kicker {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.55em;
|
||||
text-indent: 0.55em; /* balance the trailing letter-spacing */
|
||||
color: var(--clr-accent);
|
||||
}
|
||||
|
||||
.sm-boot-spinner {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 3px solid var(--clr-border-hi);
|
||||
border-top-color: var(--clr-accent);
|
||||
border-radius: 50%;
|
||||
animation: sm-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes sm-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.sm-boot-text {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.9rem;
|
||||
color: var(--clr-text-mid);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sm-boot-spinner { animation: none; }
|
||||
}
|
||||
|
||||
/* ── Loading / error states ─────────────────────────────────────────── */
|
||||
.loading {
|
||||
font-family: var(--font-mono);
|
||||
|
||||
@@ -15,7 +15,21 @@
|
||||
|
||||
<div class="status-bar-safe-area"></div>
|
||||
|
||||
<div id="app">Loading...</div>
|
||||
<!--
|
||||
In-page boot splash. Lives INSIDE #app so Blazor wipes it automatically the
|
||||
moment the root component first renders. Styled to match the native MAUI
|
||||
splash (same void bg + electric-ice accent), so handoff native -> webview ->
|
||||
Blazor reads as one continuous loading screen rather than three flashes.
|
||||
Inline-styled on the wrapper so it shows even before app.css paints.
|
||||
-->
|
||||
<div id="app" style="background:#0b0f14">
|
||||
<div class="sm-boot">
|
||||
<div class="sm-boot-title">SilverOS</div>
|
||||
<div class="sm-boot-kicker">WELCOME</div>
|
||||
<div class="sm-boot-spinner" aria-hidden="true"></div>
|
||||
<div class="sm-boot-text">Preparing your setup…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
|
||||
Reference in New Issue
Block a user