21 Commits

Author SHA1 Message Date
dfae1f136b Merge pull request 'fix(build): drop invalid --no-incremental from dotnet publish' (#33) from fix/clean-publish-flag into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 9s
2026-06-10 22:26:28 +00:00
sysadmin
74e48aa1e5 fix(build): drop invalid --no-incremental from dotnet publish (MSB1001)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 8s
dotnet publish rejects --no-incremental (it's a dotnet build switch) -> MSB1001 Unknown
switch -> build failed. The bin/obj wipe alone forces the clean recompile we need.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:26:08 +01:00
a6ac6ce355 Merge pull request 'fix(build): clean compile before publish (CI shipped a stale toolbox DLL)' (#32) from fix/ci-clean-publish into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 31s
2026-06-10 22:21:30 +00:00
sysadmin
9832121dbb fix(build): clean compile before publish (CI shipped stale Core.dll)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 1m31s
The deployed toolbox Core.dll was timestamped BEFORE its own build ran -- the CI runner's
incremental build reused a cached SilverOS.Welcome.Core.dll, so source fixes (e.g. the winget
bootstrap brace fix) never reached the published exe. Wipe all bin/obj under welcome/ and pass
--no-incremental so every build is a clean compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 23:21:04 +01:00
d0a5925652 Merge pull request 'fix(apps): winget bootstrap never ran (unbalanced-brace parse error) — the real apps-skip cause' (#31) from fix/winget-bootstrap-brace into main
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 5m32s
2026-06-10 21:44:32 +00:00
sysadmin
e91c4de7ed fix(apps): winget bootstrap never ran (unbalanced-brace parse error in inline cmd)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 36s
appinstall.log on the VM showed: bootstrap-winget exit=1 'Unexpected token }'. The
inline -Command was built from an interpolated string ($"...{{...}}" -> {/}) concatenated
with a NON-interpolated string whose '}}' stayed literal, so the emitted PowerShell ended
in '}}' and failed to parse -> the bootstrap (and thus winget install) never executed ->
all apps skipped on every run, regardless of network. Invoke the bootstrap .ps1 file
directly instead (it self-checks + installs winget online); fall back to the inbox
re-register only when the script is absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 22:44:05 +01:00
51ab88b1f8 Merge pull request 'fix(toolbox): move Done 'Restart now' button to the footer-right (was clipped)' (#30) from fix/done-restart-footer into main
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 5m41s
2026-06-10 18:12:19 +00:00
sysadmin
709744d533 feat(apps): AppInstaller writes a diagnostic log (winget resolve + bootstrap + per-app)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 2s
Writes C:\ProgramData\SilverMetal\appinstall.log (best-effort) so a post-install mount
shows exactly where app installs fail: winget probe results, bootstrap-winget output,
and per-app winget exit codes. Makes the no-apps-installed failure diagnosable instead
of inferred.
2026-06-10 19:12:11 +01:00
sysadmin
ddd8784b56 fix(toolbox): move Done 'Restart now' to footer-right (was clipped in content)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 33s
The in-content Restart button overflowed its fixed width. Move it into the wizard
footer's right slot (where Next/Apply sits) as a btn-primary; Routes owns the restart
shutdown now, DoneStep just shows the recovery key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:06:53 +01:00
226a823c68 Merge pull request 'fix: track driver .exe (NetKVM inject) + winget online bootstrap — the two app-install blockers' (#29) from fix/driver-exe-and-winget into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 4m0s
2026-06-10 16:22:52 +00:00
sysadmin
67befa56df fix(build): track driver .exe files (gitignore *.exe dropped netkvmp.exe -> DISM rejected NetKVM)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 5m49s
The global .gitignore '*.exe' rule silently excluded netkvmp.exe + netkvmco.exe when the
NetKVM driver was committed, so only inf/sys/cat shipped. netkvm.inf REQUIRES netkvmp.exe
([SourceDisksFiles] + netkvmp.CopyFiles), so Add-WindowsDriver failed every build with 'the
driver package could not be installed' -> no virtio NIC driver -> no VM network. Un-ignore
windows/drivers/** and force-add the referenced binaries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 17:22:07 +01:00
sysadmin
13df66d137 feat(apps): bootstrap-winget downloads + installs the App Installer online (LTSC lacks it) 2026-06-10 17:21:27 +01:00
541a17c792 Merge pull request 'ci(windows): free build working set before the persist copy (persist OOM)' (#28) from ci/free-before-persist into main
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 6m24s
2026-06-10 15:35:35 +00:00
sysadmin
9fa613b8c1 ci(windows): free build working set before persist copy (oscdimg OK, persist OOM)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 45s
Build got through the ISO repack but failed copying the 5GB ISO to C:\silvermetal\out
('not enough space'): the build's working set (extracted ISO tree + expanded install.wim
+ 5GB base ISO) fills the single-volume runner, leaving <5GB for the persist copy. The
image grew again with the injected driver. Delete RUNNER_TEMP\smbuild + base.iso (no
longer needed post-build/validate) right before the copy to reclaim ~10GB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:35:01 +01:00
8f61d5fb61 Merge pull request 'fix(build): driver inject non-fatal + ForceUnsigned (NetKVM rejected, bricked build)' (#27) from fix/driver-inject-resilient into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 7m24s
2026-06-10 13:41:42 +00:00
sysadmin
09e1f94b7d fix(build): driver inject non-fatal + ForceUnsigned + .gitattributes binary
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 7m35s
Add-WindowsDriver rejected the virtio NetKVM driver during offline servicing and
aborted the whole build. A driver issue must not brick the image: wrap it in try/catch
(warn + continue) and add -ForceUnsigned to bypass the offline-inject signature check
(the driver is WHQL-signed and loads at boot regardless). Add .gitattributes marking
driver/binary files as binary so the runner checkout never EOL-normalizes them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:41:22 +01:00
8ceb38c3dd Merge pull request 'fix(collector): button footer + inject virtio NIC driver (HVCI network)' (#26) from fix/collector-button-layout into main
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (push) Failing after 4m46s
2026-06-10 13:32:59 +00:00
sysadmin
a169d2a452 feat(build): inject virtio-net (NetKVM) driver for HVCI-compatible VM networking
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 4m56s
The privacy hardening enables HVCI (Memory Integrity), which blocks the legacy
e1000 NIC driver (E1G6032E.sys) -> no network in the VM, so winget app installs
silently skip. virtio-net's NetKVM driver is WHQL-signed + HVCI-compatible. Staged
from virtio-win (w11/amd64) under windows/drivers/netkvm/; build.ps1 already auto-
injects any *.inf under windows/drivers/ into install.wim. Pair with a virtio NIC on
the VM (already switched). Lets apps actually install under hardening.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:32:48 +01:00
sysadmin
20743e9b54 fix(collector): move Finish/Cancel into a clean footer (were overlapping Confirm PIN) 2026-06-10 14:29:52 +01:00
75f97778f8 Merge pull request 'feat(toolbox): first-run lands on the Apps picker (not silent auto-apply)' (#25) from fix/first-run-apps-picker into main
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 6m13s
2026-06-10 13:11:42 +00:00
sysadmin
18475fa731 feat(toolbox): first-run lands on the Apps picker (not silent auto-apply)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 6m14s
Hands-on VM testing showed auto-apply skipped the app picker entirely -- the user
couldn't review/adjust apps before install. Land first-run on the Apps step instead
(pre-checked with the collector flavour's defaults); the user adjusts then walks
Apps -> Prefs -> Apply -> Done. The collector already owns account + flavour, so
Welcome/Flavour are skipped. Reverses the earlier auto-apply behavior per operator
feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:11:28 +01:00
14 changed files with 538 additions and 27 deletions

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
# Keep binary assets verbatim (no EOL/charset normalization on checkout).
*.sys binary
*.cat binary
*.exe binary
*.dll binary
*.iso binary
*.png binary
*.jpg binary
*.ico binary
*.cab binary
windows/drivers/** binary

View File

@@ -184,6 +184,15 @@ jobs:
# RUNNER_TEMP is per-job/ephemeral. Keep the latest validated build at a
# stable path so it can be retrieved (e.g. for VM boot-testing) out of band.
New-Item -ItemType Directory -Force 'C:\silvermetal\out' | Out-Null
# The ISO is already built + validated; free the build working set (extracted ISO
# tree + the mounted/expanded install.wim + the 5GB base ISO) BEFORE the ~5GB persist
# copy, or the single-volume runner runs out of space mid-copy. The ISO itself lives
# in RUNNER_TEMP\out (untouched) and the SBOM/SHA uploads read from there too.
$before = [math]::Round((Get-PSDrive C).Free/1GB,1)
Remove-Item "$env:RUNNER_TEMP\smbuild" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$env:RUNNER_TEMP\base.iso" -Force -ErrorAction SilentlyContinue
$after = [math]::Round((Get-PSDrive C).Free/1GB,1)
Write-Host " freed build working set: C: ${before}GB -> ${after}GB before persist"
Copy-Item "$env:RUNNER_TEMP\out\*" 'C:\silvermetal\out\' -Force
Get-ChildItem 'C:\silvermetal\out' | ForEach-Object { Write-Host $_.Name }

4
.gitignore vendored
View File

@@ -68,3 +68,7 @@ coverage/
# SBOM intermediates (final SBOMs are committed; intermediates are not)
sbom/work/
# Driver binaries (e.g. virtio NetKVM netkvmp.exe) must be tracked despite the global
# *.exe / *.msi ignores above -- they are referenced by the .inf and DISM needs them.
!windows/drivers/**

View File

@@ -1,5 +1,68 @@
#Requires -Version 5.1
$ErrorActionPreference='SilentlyContinue'
# Register the inbox App Installer if present, else nothing to do (offline image w/o it).
# Provision winget (the App Installer) when absent. Windows IoT Enterprise LTSC
# ships WITHOUT the inbox Microsoft.DesktopAppInstaller package, so re-registering
# it is not enough - we download and install it (plus dependencies) online at apply
# time. Best-effort and idempotent: exit 0 if winget ends up available, else 1.
$ErrorActionPreference = 'SilentlyContinue'
function Test-Winget {
return [bool](Get-Command winget -ErrorAction SilentlyContinue)
}
# Fast path 1: winget already on PATH.
if (Test-Winget) { exit 0 }
# Fast path 2: an inbox App Installer package is present - just re-register it.
Get-AppxPackage -AllUsers Microsoft.DesktopAppInstaller |
ForEach-Object { Add-AppxPackage -DisableDevelopmentMode -Register "$($_.InstallLocation)\AppXManifest.xml" }
ForEach-Object { Add-AppxPackage -DisableDevelopmentMode -Register "$($_.InstallLocation)\AppXManifest.xml" }
if (Test-Winget) { exit 0 }
# Slow path: download + install the App Installer and its dependencies online.
$temp = $env:TEMP
$bundlePath = Join-Path $temp 'Microsoft.DesktopAppInstaller.msixbundle'
$vclibsPath = Join-Path $temp 'Microsoft.VCLibs.x64.14.00.Desktop.appx'
$uixamlNupkg = Join-Path $temp 'microsoft.ui.xaml.2.8.6.nupkg'
$uixamlExtract = Join-Path $temp 'uixaml.2.8.6'
$uixamlAppx = Join-Path $uixamlExtract 'tools\AppX\x64\Release\Microsoft.UI.Xaml.2.8.appx'
$bundleUrl = 'https://aka.ms/getwinget'
$vclibsUrl = 'https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx'
$uixamlUrl = 'https://globalcdn.nuget.org/packages/microsoft.ui.xaml.2.8.6.nupkg'
function Get-File {
param([string]$Url, [string]$Destination)
try {
Invoke-WebRequest -Uri $Url -OutFile $Destination -UseBasicParsing
return (Test-Path $Destination)
} catch {
return $false
}
}
# Download the App Installer bundle (required).
if (-not (Get-File -Url $bundleUrl -Destination $bundlePath)) { exit 1 }
# Download the VCLibs desktop dependency (required).
if (-not (Get-File -Url $vclibsUrl -Destination $vclibsPath)) { exit 1 }
# Download the UI.Xaml 2.8 nuget package (a .zip) and extract the appx from it.
if (-not (Get-File -Url $uixamlUrl -Destination $uixamlNupkg)) { exit 1 }
try {
if (Test-Path $uixamlExtract) { Remove-Item -Path $uixamlExtract -Recurse -Force }
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($uixamlNupkg, $uixamlExtract)
} catch {
exit 1
}
if (-not (Test-Path $uixamlAppx)) { exit 1 }
# Install order: VCLibs dependency, then UI.Xaml dependency, then the bundle with
# both supplied as DependencyPath. Per-user Add-AppxPackage (toolbox runs as the
# real admin user at first logon).
Add-AppxPackage -Path $vclibsPath
Add-AppxPackage -Path $uixamlAppx
Add-AppxPackage -Path $bundlePath -DependencyPath $vclibsPath, $uixamlAppx
# Final re-check.
if (Test-Winget) { exit 0 }
exit 1

View File

@@ -176,13 +176,13 @@ try {
$btnCancel = New-Object Windows.Forms.Button
$btnCancel.Text = 'Cancel'
Style-Button $btnCancel $false
$btnCancel.Location = New-Object Drawing.Point(580, ($y + 48))
$btnCancel.Location = New-Object Drawing.Point(580, ($y + 120))
$form.Controls.Add($btnCancel)
$btnFinish = New-Object Windows.Forms.Button
$btnFinish.Text = 'Finish'
Style-Button $btnFinish $true
$btnFinish.Location = New-Object Drawing.Point(750, ($y + 48))
$btnFinish.Location = New-Object Drawing.Point(750, ($y + 120))
$form.Controls.Add($btnFinish)
# --- behaviour -------------------------------------------------------------

Binary file not shown.

View File

@@ -0,0 +1,375 @@
;-------------------------------------------------------------------------------
;Copyright (c) 2008-2021 Red Hat Inc.
;
;
;Module Name:
; netkvm.inf
;
; VirtIO Ethernet Adapter
;
;-------------------------------------------------------------------------------
;Installation Notes:
; Step by step driver installation wiki:
; https://github.com/virtio-win/kvm-guest-drivers-windows/wiki/Driver-installation
;
[version]
Signature = "$Windows NT$"
Class = Net
CatalogFile = netkvm.cat
ClassGUID = {4d36e972-e325-11ce-bfc1-08002be10318}
Provider=%VENDOR%
DriverVer = 07/09/2025,100.101.104.28500
DriverPackageType = PlugAndPlay
DriverPackageDisplayName = %kvmnet6.DeviceDesc%
PnpLockDown=1
[Manufacturer]
%VENDOR% = NetKVM, NTamd64.10.0...16299
[NetKVM.NTamd64.10.0...16299]
%kvmnet6.DeviceDesc% = kvmnet6.ndi, PCI\VEN_1AF4&DEV_1000&SUBSYS_00011AF4&REV_00, PCI\VEN_1AF4&DEV_1000
%kvmnet6.DeviceDesc% = kvmnet6.ndi, PCI\VEN_1AF4&DEV_1041&SUBSYS_11001AF4&REV_01, PCI\VEN_1AF4&DEV_1041
[kvmnet6.ndi.hw]
AddReg = kvmnet6.EnableMSI
[kvmnet6.EnableMSI]
;HKR, "Interrupt Management",, 0x00000010
;HKR, "Interrupt Management\MessageSignaledInterruptProperties",, 0x00000010
HKR, "Interrupt Management\MessageSignaledInterruptProperties", MSISupported, 0x00010001, 1
HKR, "Interrupt Management\MessageSignaledInterruptProperties", MessageNumberLimit, 0x00010001, 2048
;HKR, "Interrupt Management\Affinity Policy",, 0x00000010
HKR, "Interrupt Management\Affinity Policy", DevicePolicy, 0x00010001, 0
HKR, "Interrupt Management\Affinity Policy", DevicePriority, 0x00010001, 2
[kvmnet6.ndi]
Characteristics = 0x84 ; NCF_PHYSICAL | NCF_HAS_UI
BusType = 5 ; PCI
AddReg = kvmnet6.Reg, Parameters
CopyFiles = kvmnet6.CopyFiles, netkvmp.CopyFiles
*IfType = 6
*MediaType = 0 ; NdisMedium802_3
*PhysicalMediaType = 0 ; NdisPhysicalMediumUnspecified
[kvmnet6.ndi.Services]
AddService = netkvm, 2, kvmnet6.Service, kvmnet6.EventLog
;-----------------------------------------------------------------------------
; Red Hat ParaVirtualized Miniport Common
;-----------------------------------------------------------------------------
[Parameters]
HKR, Ndi\Params\Priority, ParamDesc, 0, %Priority%
HKR, Ndi\Params\Priority, Default, 0, "1"
HKR, Ndi\Params\Priority, type, 0, "enum"
HKR, Ndi\Params\Priority\enum, "1", 0, %Enable%
HKR, Ndi\Params\Priority\enum, "0", 0, %Disable%
HKR, Ndi\Params\*PriorityVLANTag, ParamDesc, 0, %PriorityVlanTag%
HKR, Ndi\Params\*PriorityVLANTag, Default, 0, "3"
HKR, Ndi\Params\*PriorityVLANTag, type, 0, "enum"
HKR, Ndi\Params\*PriorityVLANTag\enum, "3", 0, %Priority_Vlan%
HKR, Ndi\Params\*PriorityVLANTag\enum, "2", 0, %VLan%
HKR, Ndi\Params\*PriorityVLANTag\enum, "1", 0, %PriorityOnly%
HKR, Ndi\Params\*PriorityVLANTag\enum, "0", 0, %Disable%
HKR, Ndi\params\VlanID, ParamDesc, 0, %VLan_ID%
HKR, Ndi\params\VlanID, type, 0, "long"
HKR, Ndi\params\VlanID, default, 0, "0"
HKR, Ndi\params\VlanID, min, 0, "0"
HKR, Ndi\params\VlanID, max, 0, "4094"
HKR, Ndi\Params\DoLog, ParamDesc, 0, %EnableLogging%
HKR, Ndi\Params\DoLog, Default, 0, "1"
HKR, Ndi\Params\DoLog, type, 0, "enum"
HKR, Ndi\Params\DoLog\enum, "1", 0, %Enable%
HKR, Ndi\Params\DoLog\enum, "0", 0, %Disable%
HKR, Ndi\params\DebugLevel, ParamDesc, 0, %DebugLevel%
HKR, Ndi\params\DebugLevel, type, 0, "int"
HKR, Ndi\params\DebugLevel, default, 0, "0"
HKR, Ndi\params\DebugLevel, min, 0, "0"
HKR, Ndi\params\DebugLevel, max, 0, "8"
HKR, Ndi\params\DebugLevel, step, 0, "1"
HKR, Ndi\params\*JumboPacket, ParamDesc, 0, %JumboPacket%
HKR, Ndi\params\*JumboPacket, type, 0, "long"
HKR, Ndi\params\*JumboPacket, default, 0, "1514"
HKR, Ndi\params\*JumboPacket, min, 0, "590"
HKR, Ndi\params\*JumboPacket, max, 0, "65500"
HKR, Ndi\params\*JumboPacket, step, 0, "1"
HKR, Ndi\params\TxCapacity, ParamDesc, 0, %TxCapacity%
HKR, Ndi\params\TxCapacity, type, 0, "enum"
HKR, Ndi\params\TxCapacity, default, 0, "1024"
HKR, Ndi\Params\TxCapacity\enum, "16", 0, %String_16%
HKR, Ndi\Params\TxCapacity\enum, "32", 0, %String_32%
HKR, Ndi\Params\TxCapacity\enum, "64", 0, %String_64%
HKR, Ndi\Params\TxCapacity\enum, "128", 0, %String_128%
HKR, Ndi\Params\TxCapacity\enum, "256", 0, %String_256%
HKR, Ndi\Params\TxCapacity\enum, "512", 0, %String_512%
HKR, Ndi\Params\TxCapacity\enum, "1024", 0, %String_1024%
HKR, Ndi\params\RxCapacity, ParamDesc, 0, %RxCapacity%
HKR, Ndi\params\RxCapacity, type, 0, "enum"
HKR, Ndi\params\RxCapacity, default, 0, "1024"
HKR, Ndi\Params\RxCapacity\enum, "16", 0, %String_16%
HKR, Ndi\Params\RxCapacity\enum, "32", 0, %String_32%
HKR, Ndi\Params\RxCapacity\enum, "64", 0, %String_64%
HKR, Ndi\Params\RxCapacity\enum, "128", 0, %String_128%
HKR, Ndi\Params\RxCapacity\enum, "256", 0, %String_256%
HKR, Ndi\Params\RxCapacity\enum, "512", 0, %String_512%
HKR, Ndi\Params\RxCapacity\enum, "1024", 0, %String_1024%
HKR, Ndi\Params\RxCapacity\enum, "2048", 0, %String_2048%
HKR, Ndi\Params\RxCapacity\enum, "4096", 0, %String_4096%
HKR, Ndi\Params\SeparateTail, ParamDesc, 0, %SeparateTail%
HKR, Ndi\Params\SeparateTail, Default, 0, "1"
HKR, Ndi\Params\SeparateTail, type, 0, "enum"
HKR, Ndi\Params\SeparateTail\enum, "1", 0, %Enable%
HKR, Ndi\Params\SeparateTail\enum, "0", 0, %Disable%
HKR, Ndi\Params\FastInit, ParamDesc, 0, %FastInit%
HKR, Ndi\Params\FastInit, Default, 0, "1"
HKR, Ndi\Params\FastInit, type, 0, "enum"
HKR, Ndi\Params\FastInit\enum, "1", 0, %Enable%
HKR, Ndi\Params\FastInit\enum, "0", 0, %Disable%
HKR, Ndi\params\NetworkAddress, ParamDesc, 0, %NetworkAddress%
HKR, Ndi\params\NetworkAddress, type, 0, "edit"
HKR, Ndi\params\NetworkAddress, Optional, 0, "1"
HKR, Ndi\Params\OffLoad.TxChecksum, ParamDesc, 0, %OffLoad.TxChecksum%
HKR, Ndi\Params\OffLoad.TxChecksum, Default, 0, "31"
HKR, Ndi\Params\OffLoad.TxChecksum, type, 0, "enum"
HKR, Ndi\Params\OffLoad.TxChecksum\enum, "31", 0, %All%
HKR, Ndi\Params\OffLoad.TxChecksum\enum, "27", 0, %TCPUDPAll%
HKR, Ndi\Params\OffLoad.TxChecksum\enum, "3", 0, %TCPUDPv4%
HKR, Ndi\Params\OffLoad.TxChecksum\enum, "1", 0, %TCPv4%
HKR, Ndi\Params\OffLoad.TxChecksum\enum, "0", 0, %Disable%
HKR, Ndi\Params\OffLoad.TxLSO, ParamDesc, 0, %OffLoad.TxLSO%
HKR, Ndi\Params\OffLoad.TxLSO, Default, 0, "2"
HKR, Ndi\Params\OffLoad.TxLSO, type, 0, "enum"
HKR, Ndi\Params\OffLoad.TxLSO\enum, "2", 0, %Maximal%
HKR, Ndi\Params\OffLoad.TxLSO\enum, "1", 0, %IPv4%
HKR, Ndi\Params\OffLoad.TxLSO\enum, "0", 0, %Disable%
HKR, Ndi\Params\OffLoad.RxCS, ParamDesc, 0, %OffLoad.RxCS%
HKR, Ndi\Params\OffLoad.RxCS, Default, 0, "31"
HKR, Ndi\Params\OffLoad.RxCS, type, 0, "enum"
HKR, Ndi\Params\OffLoad.RxCS\enum, "31", 0, %All%
HKR, Ndi\Params\OffLoad.RxCS\enum, "27", 0, %TCPUDPAll%
HKR, Ndi\Params\OffLoad.RxCS\enum, "3", 0, %TCPUDPv4%
HKR, Ndi\Params\OffLoad.RxCS\enum, "1", 0, %TCPv4%
HKR, Ndi\Params\OffLoad.RxCS\enum, "0", 0, %Disable%
HKR, Ndi\Params\*IPChecksumOffloadIPv4, ParamDesc, 0, %Std.IPChecksumOffloadv4%
HKR, Ndi\Params\*IPChecksumOffloadIPv4, Default, 0, "3"
HKR, Ndi\Params\*IPChecksumOffloadIPv4, type, 0, "enum"
HKR, Ndi\Params\*IPChecksumOffloadIPv4\enum, "3", 0, %TxRx%
HKR, Ndi\Params\*IPChecksumOffloadIPv4\enum, "2", 0, %Rx%
HKR, Ndi\Params\*IPChecksumOffloadIPv4\enum, "1", 0, %Tx%
HKR, Ndi\Params\*IPChecksumOffloadIPv4\enum, "0", 0, %Disable%
HKR, Ndi\Params\*LsoV2IPv4, ParamDesc, 0, %Std.LsoV2IPv4%
HKR, Ndi\Params\*LsoV2IPv4, Default, 0, "1"
HKR, Ndi\Params\*LsoV2IPv4, type, 0, "enum"
HKR, Ndi\Params\*LsoV2IPv4\enum, "1", 0, %Enable%
HKR, Ndi\Params\*LsoV2IPv4\enum, "0", 0, %Disable%
HKR, Ndi\Params\*LsoV2IPv6, ParamDesc, 0, %Std.LsoV2IPv6%
HKR, Ndi\Params\*LsoV2IPv6, Default, 0, "1"
HKR, Ndi\Params\*LsoV2IPv6, type, 0, "enum"
HKR, Ndi\Params\*LsoV2IPv6\enum, "1", 0, %Enable%
HKR, Ndi\Params\*LsoV2IPv6\enum, "0", 0, %Disable%
HKR, Ndi\Params\*UDPChecksumOffloadIPv4, ParamDesc, 0, %Std.UDPChecksumOffloadIPv4%
HKR, Ndi\Params\*UDPChecksumOffloadIPv4, Default, 0, "3"
HKR, Ndi\Params\*UDPChecksumOffloadIPv4, type, 0, "enum"
HKR, Ndi\Params\*UDPChecksumOffloadIPv4\enum, "3", 0, %TxRx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv4\enum, "2", 0, %Rx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv4\enum, "1", 0, %Tx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv4\enum, "0", 0, %Disable%
HKR, Ndi\Params\*TCPChecksumOffloadIPv4, ParamDesc, 0, %Std.TCPChecksumOffloadIPv4%
HKR, Ndi\Params\*TCPChecksumOffloadIPv4, Default, 0, "3"
HKR, Ndi\Params\*TCPChecksumOffloadIPv4, type, 0, "enum"
HKR, Ndi\Params\*TCPChecksumOffloadIPv4\enum, "3", 0, %TxRx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv4\enum, "2", 0, %Rx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv4\enum, "1", 0, %Tx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv4\enum, "0", 0, %Disable%
HKR, Ndi\Params\*TCPChecksumOffloadIPv6, ParamDesc, 0, %Std.TCPChecksumOffloadIPv6%
HKR, Ndi\Params\*TCPChecksumOffloadIPv6, Default, 0, "3"
HKR, Ndi\Params\*TCPChecksumOffloadIPv6, type, 0, "enum"
HKR, Ndi\Params\*TCPChecksumOffloadIPv6\enum, "3", 0, %TxRx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv6\enum, "2", 0, %Rx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv6\enum, "1", 0, %Tx%
HKR, Ndi\Params\*TCPChecksumOffloadIPv6\enum, "0", 0, %Disable%
HKR, Ndi\Params\*UDPChecksumOffloadIPv6, ParamDesc, 0, %Std.UDPChecksumOffloadIPv6%
HKR, Ndi\Params\*UDPChecksumOffloadIPv6, Default, 0, "3"
HKR, Ndi\Params\*UDPChecksumOffloadIPv6, type, 0, "enum"
HKR, Ndi\Params\*UDPChecksumOffloadIPv6\enum, "3", 0, %TxRx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv6\enum, "2", 0, %Rx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv6\enum, "1", 0, %Tx%
HKR, Ndi\Params\*UDPChecksumOffloadIPv6\enum, "0", 0, %Disable%
HKR, Ndi\params\MinRxBufferPercent, ParamDesc, 0, %MinRxBufferPercent%
HKR, Ndi\params\MinRxBufferPercent, type, 0, "int"
HKR, Ndi\params\MinRxBufferPercent, default, 0, "0"
HKR, Ndi\params\MinRxBufferPercent, min, 0, "0"
HKR, Ndi\params\MinRxBufferPercent, max, 0, "100"
HKR, Ndi\params\MinRxBufferPercent, step, 0, "1"
[kvmnet6.CopyFiles]
netkvm.sys,,,2
[netkvmp.CopyFiles]
netkvmp.exe,,,2
[kvmnet6.Service]
DisplayName = %kvmnet6.Service.DispName%
ServiceType = 1 ;%SERVICE_KERNEL_DRIVER%
StartType = 3 ;%SERVICE_DEMAND_START%
ErrorControl = 1 ;%SERVICE_ERROR_NORMAL%
ServiceBinary = %13%\netkvm.sys
LoadOrderGroup = NDIS
AddReg = TextModeFlags.Reg
[kvmnet6.EventLog]
AddReg = kvmnet6.AddEventLog.Reg
[kvmnet6.AddEventLog.Reg]
HKR, , EventMessageFile, 0x00020000, "%%SystemRoot%%\System32\netevent.dll"
HKR, , TypesSupported, 0x00010001, 7
[TextModeFlags.Reg]
HKR,,TextModeFlags,0x00010001, 0x0001
HKR,Parameters,DisableMSI,,"0"
HKR,Parameters,EarlyDebug,,"3"
HKR,Parameters,DmaRemappingCompatible,0x00010001,2
[SourceDisksNames]
1 = %DiskId1%,,,""
[SourceDisksFiles]
netkvm.sys = 1,,
netkvmp.exe = 1,,
[DestinationDirs]
kvmnet6.CopyFiles = 13
netkvmp.CopyFiles = 11
[Strings]
VENDOR = "Red Hat, Inc."
kvmnet6.DeviceDesc = "Red Hat VirtIO Ethernet Adapter"
kvmnet6.Service.DispName = "Red Hat VirtIO Ethernet Adapter Service"
DiskId1 = "Red Hat VirtIO Ethernet Adapter Driver Disk #1"
NetworkAddress = "Assign MAC"
Priority = "Init.Do802.1PQ"
JumboPacket = "Jumbo Packet"
TxCapacity = "Init.MaxTxBuffers"
RxCapacity = "Init.MaxRxBuffers"
SeparateTail = "Init.SeparateRxTail"
FastInit = "Fast Initialization"
Offload.TxChecksum = "Offload.Tx.Checksum"
Offload.TxLSO = "Offload.Tx.LSO"
Offload.RxCS = "Offload.Rx.Checksum"
EnableLogging = "Logging.Enable"
DebugLevel = "Logging.Level"
Tx = "Tx Enabled";
Rx = "Rx Enabled";
TxRx = "Rx & Tx Enabled";
Std.LsoV2IPv4 = "Large Send Offload V2 (IPv4)"
Std.LsoV2IPv6 = "Large Send Offload V2 (IPv6)"
Std.UDPChecksumOffloadIPv4 = "UDP Checksum Offload (IPv4)"
Std.TCPChecksumOffloadIPv4 = "TCP Checksum Offload (IPv4)"
Std.UDPChecksumOffloadIPv6 = "UDP Checksum Offload (IPv6)"
Std.TCPChecksumOffloadIPv6 = "TCP Checksum Offload (IPv6)"
Std.IPChecksumOffloadv4 = "IPv4 Checksum Offload"
Disable = "Disabled"
Enable = "Enabled"
Enable* = "Enabled*"
String_16 = "16"
String_32 = "32"
String_64 = "64"
String_128 = "128"
String_256 = "256"
String_512 = "512"
String_1024 = "1024"
String_2048 = "2048"
String_4096 = "4096"
PriorityVlanTag = "Priority and VLAN tagging"
PriorityOnly = "Priority"
VLan = "VLan"
VLan_ID = "VLan ID"
Priority_Vlan = "All"
10M = "10M"
100M = "100M"
1G = "1G"
10G = "10G"
TCPv4 = "TCP(v4)"
TCPUDPv4 = "TCP/UDP(v4)"
TCPUDPAll = "TCP/UDP(v4,v6)"
All = "All"
IPv4 = "IPv4"
Maximal = "Maximal"
MinRxBufferPercent = "MinRxBufferPercent"
[kvmnet6.Reg]
HKR, , BusNumber, 0, "0"
HKR, Ndi, Service, 0, "netkvm"
HKR, Ndi\Interfaces, UpperRange, 0, "ndis5"
HKR, Ndi\Interfaces, LowerRange, 0, "ethernet"
HKR, Ndi\params\*RSS, ParamDesc, 0, "Receive Side Scaling"
HKR, Ndi\params\*RSS, Type, 0, "enum"
HKR, Ndi\params\*RSS, Default, 0, "1"
HKR, Ndi\params\*RSS, Optional, 0, "0"
HKR, Ndi\params\*RSS\enum, "0", 0, "Disabled"
HKR, Ndi\params\*RSS\enum, "1", 0, "Enabled"
HKR, Ndi\params\*NumRssQueues, ParamDesc, 0, "Maximum Number of RSS Queues"
HKR, Ndi\params\*NumRssQueues, type, 0, "int"
HKR, Ndi\params\*NumRssQueues, default, 0, "16"
HKR, Ndi\params\*NumRssQueues, min, 0, "1"
HKR, Ndi\params\*NumRssQueues, max, 0, "32"
HKR, Ndi\params\*NumRssQueues, step, 0, "1"
HKR, Ndi\params\*RscIPv4, ParamDesc, 0, "Recv Segment Coalescing (IPv4)"
HKR, Ndi\params\*RscIPv4, Type, 0, "enum"
HKR, Ndi\params\*RscIPv4, Default, 0, "1"
HKR, Ndi\params\*RscIPv4, Optional, 0, "0"
HKR, Ndi\params\*RscIPv4\enum, "0", 0, "Disabled"
HKR, Ndi\params\*RscIPv4\enum, "1", 0, "Enabled"
HKR, Ndi\params\*RscIPv6, ParamDesc, 0, "Recv Segment Coalescing (IPv6)"
HKR, Ndi\params\*RscIPv6, Type, 0, "enum"
HKR, Ndi\params\*RscIPv6, Default, 0, "1"
HKR, Ndi\params\*RscIPv6, Optional, 0, "0"
HKR, Ndi\params\*RscIPv6\enum, "0", 0, "Disabled"
HKR, Ndi\params\*RscIPv6\enum, "1", 0, "Enabled"
HKR, Ndi\params\*UsoIPv4, ParamDesc, 0, "UDP Segmentation Offload (IPv4)"
HKR, Ndi\params\*UsoIPv4, Type, 0, "enum"
HKR, Ndi\params\*UsoIPv4, Default, 0, "1"
HKR, Ndi\params\*UsoIPv4, Optional, 0, "0"
HKR, Ndi\params\*UsoIPv4\enum, "0", 0, "Disabled"
HKR, Ndi\params\*UsoIPv4\enum, "1", 0, "Enabled"
HKR, Ndi\params\*UsoIPv6, ParamDesc, 0, "UDP Segmentation Offload (IPv6)"
HKR, Ndi\params\*UsoIPv6, Type, 0, "enum"
HKR, Ndi\params\*UsoIPv6, Default, 0, "1"
HKR, Ndi\params\*UsoIPv6, Optional, 0, "0"
HKR, Ndi\params\*UsoIPv6\enum, "0", 0, "Disabled"
HKR, Ndi\params\*UsoIPv6\enum, "1", 0, "Enabled"
HKR, Ndi\params\*NdisPoll, ParamDesc, 0, "Ndis Poll Mode"
HKR, Ndi\params\*NdisPoll, Type, 0, "enum"
HKR, Ndi\params\*NdisPoll, Default, 0, "1"
HKR, Ndi\params\*NdisPoll, Optional, 0, "0"
HKR, Ndi\params\*NdisPoll\enum, "0", 0, "Disabled"
HKR, Ndi\params\*NdisPoll\enum, "1", 0, "Enabled"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -160,6 +160,12 @@ function Invoke-PublishWelcome {
Write-Stage 'Stage 3b: publish SilverOS Welcome app (win-x64 self-contained)'
$proj = Join-Path $WindowsDir 'welcome\src\SilverOS.Welcome.App'
$out = Join-Path $WorkDir 'welcome-publish'
# Force a CLEAN compile. The CI runner reuses build artifacts across runs, and dotnet's
# incremental build has shipped a STALE SilverOS.Welcome.Core.dll (old code despite fixed
# source) -- so wipe every bin/obj under welcome/ before publishing (a clean tree forces a
# full recompile; note `dotnet publish` does NOT accept --no-incremental).
Get-ChildItem (Join-Path $WindowsDir 'welcome') -Recurse -Directory -EA SilentlyContinue |
Where-Object { $_.Name -in 'bin', 'obj' } | Remove-Item -Recurse -Force -EA SilentlyContinue
& dotnet publish $proj -c Release -f net9.0-windows10.0.19041.0 -r win-x64 --self-contained true -o $out
if ($LASTEXITCODE -ne 0) { throw 'Welcome app dotnet publish failed' }
Write-Host " Published to: $out"
@@ -254,7 +260,13 @@ function Invoke-ServiceWim {
# Drivers (GPD Pocket 4 pack) -- skipped silently if dir empty (e.g. VM test).
$drv = Join-Path $WindowsDir 'drivers'
if ((Get-ChildItem $drv -Recurse -Filter *.inf -EA SilentlyContinue)) {
Write-Host ' adding drivers'; Add-WindowsDriver -Path $mount -Driver $drv -Recurse | Out-Null
# -ForceUnsigned: skip the offline-inject signature check (the virtio NetKVM
# driver is WHQL-signed and loads fine at boot; the offline check can still
# reject it on the build host). Non-fatal: a driver issue must not brick the
# whole image build -- warn and continue without it.
Write-Host ' adding drivers'
try { Add-WindowsDriver -Path $mount -Driver $drv -Recurse -ForceUnsigned -ErrorAction Stop | Out-Null }
catch { Write-Warning " driver inject failed (continuing without it): $($_.Exception.Message)" }
} else { Write-Host ' no .inf drivers staged (ok for VM test)' }
# Kiosk features (Shell Launcher v2 + Keyboard Filter) — IoT Enterprise LTSC.

View File

@@ -4,17 +4,38 @@ namespace SilverOS.Welcome.Core.Apps;
public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppInstaller
{
// Best-effort diagnostic log (winget resolution, bootstrap output, per-app results).
// Lives under ProgramData so it survives + is readable post-install. Never throws.
private static readonly string LogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "SilverMetal", "appinstall.log");
private static void Log(string msg)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
File.AppendAllText(LogPath, $"{DateTime.Now:HH:mm:ss.fff} {msg}{Environment.NewLine}");
}
catch { /* logging is best-effort */ }
}
private static string Snip(string? s) =>
string.IsNullOrWhiteSpace(s) ? "" : s.Trim().Replace("\r", " ").Replace("\n", " ") is var t && t.Length > 300 ? t[..300] : t;
public async Task<IReadOnlyList<AppInstallResult>> InstallAsync(
IReadOnlyList<AppCatalogEntry> apps, IProgress<ApplyProgress> progress, CancellationToken ct = default)
{
var results = new List<AppInstallResult>();
if (apps.Count == 0) return results;
Log($"InstallAsync: {apps.Count} app(s) requested: {string.Join(", ", apps.Select(a => a.Id))}");
// App installs are non-critical: a missing/broken winget (e.g. offline IoT LTSC) must
// NEVER fail onboarding. Resolve winget defensively; if it can't be found, skip installs.
var winget = await ResolveWingetAsync(progress, ct);
if (winget is null)
{
Log($"winget UNAVAILABLE -> skipping all {apps.Count} app(s)");
progress.Report(new($"App installer unavailable - skipping {apps.Count} app(s)", 80));
foreach (var app in apps) results.Add(new AppInstallResult(app.Id, false));
return results;
@@ -33,6 +54,7 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
$"install --id {id} --silent --accept-package-agreements --accept-source-agreements --disable-interactivity",
ct);
ok = r.ExitCode == 0;
Log($"install {id}: exit={r.ExitCode} ok={ok} err={Snip(r.StdErr)}");
if (ok && !string.IsNullOrWhiteSpace(app.Configure))
{
var script = Path.Combine(appsDir, "configure", app.Configure);
@@ -43,6 +65,7 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
}
results.Add(new AppInstallResult(app.Id, ok));
}
Log($"InstallAsync done: {results.Count(r => r.Installed)}/{results.Count} installed");
return results;
}
@@ -52,22 +75,34 @@ public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppIn
private async Task<string?> ResolveWingetAsync(IProgress<ApplyProgress> progress, CancellationToken ct)
{
// 1) Already launchable by name (on PATH for this process)?
if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";
var p1 = await TryRunAsync("winget", "--version", ct);
Log($"winget probe (PATH): exit={p1.ExitCode} out={Snip(p1.StdOut)}");
if (p1.ExitCode == 0) return "winget";
// 2) Provision App Installer via the bundled bootstrap (or registered package), then re-probe.
// 2) Provision App Installer, then re-probe. Run the bootstrap SCRIPT FILE directly
// (it checks for winget and installs it online if absent). Invoking the .ps1 file
// avoids an inline -Command (a prior inline if/else had an unbalanced-brace parse bug
// from a non-interpolated string, so the bootstrap never actually ran).
progress.Report(new("Preparing app installer", 68));
var bootstrap = Path.Combine(appsDir, "bootstrap-winget.ps1");
await TryRunAsync("powershell.exe",
$"-NoProfile -ExecutionPolicy Bypass -Command \"if (Test-Path '{bootstrap}') {{ & '{bootstrap}' }} else {{ " +
"Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue }}\"",
ct);
if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";
var b = File.Exists(bootstrap)
? await TryRunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -File \"{bootstrap}\"", ct)
: await TryRunAsync("powershell.exe",
"-NoProfile -ExecutionPolicy Bypass -Command \"Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue\"",
ct);
Log($"bootstrap-winget: exit={b.ExitCode} out={Snip(b.StdOut)} err={Snip(b.StdErr)}");
var p2 = await TryRunAsync("winget", "--version", ct);
Log($"winget probe (post-bootstrap): exit={p2.ExitCode} out={Snip(p2.StdOut)}");
if (p2.ExitCode == 0) return "winget";
// 3) Fall back to the WindowsApps execution-alias path (bare-name launch can fail under
// UseShellExecute=false even when winget is installed).
var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var aliased = Path.Combine(local, "Microsoft", "WindowsApps", "winget.exe");
if (File.Exists(aliased) && (await TryRunAsync(aliased, "--version", ct)).ExitCode == 0) return aliased;
var aliasExists = File.Exists(aliased);
var p3Exit = aliasExists ? (await TryRunAsync(aliased, "--version", ct)).ExitCode : -1;
Log($"winget alias path '{aliased}': exists={aliasExists} probe={p3Exit}");
if (aliasExists && p3Exit == 0) return aliased;
return null;
}

View File

@@ -6,6 +6,7 @@
@inject IAppCatalog AppCatalog
@inject IPreconfigStore PreconfigStore
@inject WizardState State
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
@if (_toolboxHome)
{
@@ -83,6 +84,10 @@ else
@(_currentStep == _stepTitles.Length - 2 ? "Apply" : "Next")
</button>
}
else if (_currentStep == _stepTitles.Length - 1)
{
<button class="btn-primary" @onclick="RestartNow">Restart now</button>
}
</div>
</div>
}
@@ -108,6 +113,11 @@ else
private string? _error;
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
private async Task RestartNow()
{
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", default);
}
private bool CanGoNext => _currentStep switch
{
1 => State.Flavour is not null,
@@ -169,11 +179,11 @@ else
if (pre.Bitlocker.Enable && !string.IsNullOrEmpty(pre.Bitlocker.Pin))
State.BitLockerPin = pre.Bitlocker.Pin;
// First run with a preconfig: skip the manual walkthrough. Jump straight to the
// Apply step and signal it to auto-start (spec §4d: auto-runs once, shows progress
// + recovery key, then Done).
_currentStep = 4;
_autoApply = true;
// First run with a preconfig: the collector already captured account + flavour, so
// skip Welcome/Flavour and land on the Apps step (pre-checked with the flavour's
// defaults) so the user can review/adjust the app selection before applying. From
// there it's Apps -> Prefs -> Apply -> Done. (No auto-apply: the picker is the point.)
_currentStep = 2;
}
private void ReRunSetup()

View File

@@ -1,5 +1,4 @@
@using QRCoder
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
<div class="step done-step">
<h1>All Done!</h1>
@@ -27,8 +26,6 @@
</small></p>
</div>
}
<button class="btn-primary btn-restart" @onclick="RestartNow">Restart Now</button>
</div>
@code {
@@ -56,9 +53,4 @@
catch { /* QR is best-effort; the key text still shows */ }
}
}
private async Task RestartNow()
{
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", CancellationToken.None);
}
}