fix(welcome): harden kiosk chrome + add startup/WebView2 diagnostics
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 5m29s

4th VM e2e: kiosk shell engages + the app launches fullscreen, but the Blazor
wizard renders BLANK and the kiosk chrome didn't apply (title bar present) — the
app didn't crash, so there's no log to read. Two changes:

1) ApplyKioskChrome made defensive (null-guard HWND/AppWindow, FullScreen presenter
   only, returns bool) and wrapped in try/catch at the call site, so a chrome
   failure can never stall app/WebView startup (the likely cause of the blank).
2) Always-on file log at C:\ProgramData\SilverMetal\welcome.log: app ctor, window
   create, chrome result, unhandled exceptions, and the BlazorWebView/WebView2
   lifecycle (Initialized, NavigationCompleted, ProcessFailed). If the wizard is
   still blank next run, this pinpoints whether WebView2 env creation failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-09 18:09:24 +01:00
parent 37bfbae2e2
commit 159cea0019
5 changed files with 86 additions and 14 deletions

View File

@@ -1,20 +1,40 @@
namespace SilverOS.Welcome.App;
namespace SilverOS.Welcome.App;
public partial class App : Application
{
public App()
{
Diag.Log("App ctor");
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
Diag.Log("UNHANDLED: " + (e.ExceptionObject as Exception)?.ToString());
TaskScheduler.UnobservedTaskException += (s, e) =>
{
Diag.Log("UNOBSERVED TASK: " + e.Exception);
e.SetObserved();
};
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
Diag.Log("CreateWindow");
var window = new Window(new MainPage()) { Title = "SilverMetal Windows" };
#if WINDOWS
// Apply kiosk chrome once the native window handler exists. Wrapped so a
// chrome failure can NEVER break app/WebView startup (a previous version
// threw here and left the wizard blank). HandlerChanged can fire before the
// HWND is ready, so ApplyKioskChrome null-guards and re-applies safely.
window.HandlerChanged += (s, e) =>
{
if (window.Handler?.PlatformView is Microsoft.UI.Xaml.Window native)
native.ApplyKioskChrome();
try
{
if (window.Handler?.PlatformView is Microsoft.UI.Xaml.Window native)
{
var ok = native.ApplyKioskChrome();
Diag.Log($"ApplyKioskChrome applied={ok}");
}
}
catch (Exception ex) { Diag.Log("ApplyKioskChrome FAILED: " + ex); }
};
#endif
return window;

View File

@@ -0,0 +1,24 @@
namespace SilverOS.Welcome.App;
// Lightweight always-on file logger for first-boot diagnosis. The Welcome app
// runs as the kiosk shell with no console and (in Release) no debugger, so when
// something fails silently (blank WebView, chrome not applied) there is nowhere
// to look. This writes to a world-writable ProgramData path that survives the
// session and can be read off the disk image.
public static class Diag
{
const string Dir = @"C:\ProgramData\SilverMetal";
const string Path = Dir + @"\welcome.log";
static readonly object _lock = new();
public static void Log(string msg)
{
try
{
System.IO.Directory.CreateDirectory(Dir);
lock (_lock)
System.IO.File.AppendAllText(Path, $"{DateTime.Now:HH:mm:ss.fff} {msg}{Environment.NewLine}");
}
catch { /* logging must never throw */ }
}
}

View File

@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SilverOS.Welcome.App"
xmlns:components="clr-namespace:SilverOS.Welcome.App.Components;assembly=SilverOS.Welcome.UI"
x:Class="SilverOS.Welcome.App.MainPage">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html"
BlazorWebViewInitialized="OnBlazorInitialized"
UrlLoading="OnUrlLoading">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type components:Routes}" />
</BlazorWebView.RootComponents>

View File

@@ -1,9 +1,34 @@
namespace SilverOS.Welcome.App;
using Microsoft.AspNetCore.Components.WebView;
namespace SilverOS.Welcome.App;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
Diag.Log("MainPage ctor");
}
// Fires once the platform WebView2 is created. If this never appears in the log,
// WebView2 environment creation failed (the real cause of a blank wizard).
void OnBlazorInitialized(object? sender, BlazorWebViewInitializedEventArgs e)
{
Diag.Log("BlazorWebViewInitialized");
#if WINDOWS
try
{
var wv = e.WebView; // Microsoft.UI.Xaml.Controls.WebView2
wv.NavigationCompleted += (a, b) =>
Diag.Log($"WV2 NavigationCompleted ok={b.IsSuccess} status={b.WebErrorStatus}");
if (wv.CoreWebView2 is not null)
wv.CoreWebView2.ProcessFailed += (a, b) =>
Diag.Log("WV2 ProcessFailed: " + b.ProcessFailedKind);
}
catch (Exception ex) { Diag.Log("WV2 hook failed: " + ex.Message); }
#endif
}
void OnUrlLoading(object? sender, UrlLoadingEventArgs e)
=> Diag.Log("UrlLoading: " + e.Url);
}

View File

@@ -7,20 +7,21 @@ namespace SilverOS.Welcome.App;
public static class WindowExtensions
{
// Borderless, fullscreen, non-closable kiosk window.
public static void ApplyKioskChrome(this Microsoft.UI.Xaml.Window winuiWindow)
// Borderless, fullscreen, non-closable kiosk window. Returns true when applied.
// Defensive: HandlerChanged can fire before the HWND/AppWindow is ready, so we
// bail out cleanly and rely on a later HandlerChanged to apply it.
public static bool ApplyKioskChrome(this Microsoft.UI.Xaml.Window winuiWindow)
{
var hwnd = WindowNative.GetWindowHandle(winuiWindow);
if (hwnd == IntPtr.Zero) return false;
var id = Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = AppWindow.GetFromWindowId(id);
if (appWindow.Presenter is OverlappedPresenter p)
{
p.SetBorderAndTitleBar(false, false);
p.IsResizable = false; p.IsMaximizable = false; p.IsMinimizable = false;
}
if (appWindow is null) return false;
// FullScreen presenter is borderless (no title bar) by nature — simpler and
// more reliable than toggling OverlappedPresenter border/title-bar flags.
appWindow.SetPresenter(AppWindowPresenterKind.FullScreen);
// Block the close box; the wizard exits by rebooting, not by closing.
appWindow.Closing += (s, e) => e.Cancel = true;
return true;
}
}
#endif