Merge remote-tracking branch 'origin/main' into feat/first-boot-branding
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m35s
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m35s
# Conflicts: # windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs
This commit is contained in:
@@ -14,6 +14,11 @@ public sealed class BitLockerService(IProcessRunner runner) : IBitLockerService
|
||||
// 3. Remove any TPM-only protector (only once a TPM+PIN protector is confirmed present)
|
||||
// so the device actually requires the PIN at pre-boot.
|
||||
var script = string.Concat(
|
||||
// Eject optical install media first — BitLocker -TpmAndPinProtector refuses to enroll
|
||||
// while bootable CD/DVD media is present ("detected bootable media in the computer").
|
||||
"try { $s=New-Object -ComObject Shell.Application; ",
|
||||
"$s.Namespace(17).Items() | Where-Object { $_.Type -match 'CD|DVD' } | ForEach-Object { try { $_.InvokeVerb('Eject') } catch {} } } catch {}; ",
|
||||
"Start-Sleep -Seconds 3; ",
|
||||
"$fve='HKLM:\\SOFTWARE\\Policies\\Microsoft\\FVE'; ",
|
||||
"New-Item -Path $fve -Force | Out-Null; ",
|
||||
"New-ItemProperty -Path $fve -Name UseAdvancedStartup -Value 1 -PropertyType DWord -Force | Out-Null; ",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
namespace SilverOS.Welcome.Core.Apply;
|
||||
public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService
|
||||
{
|
||||
// Kiosk revert is BEST-EFFORT (like TearDownAsync): -EA SilentlyContinue throughout.
|
||||
// If WESL is unavailable the real user still gets Explorer (no custom shell for their
|
||||
// SID). Intentional: don't fail the apply over a missing WMI class. Must run BEFORE
|
||||
// TearDownAsync so the sm-bootstrap SID still resolves.
|
||||
public async Task RevertKioskAsync(CancellationToken ct = default)
|
||||
{
|
||||
// -EA SilentlyContinue throughout: Shell Launcher revert is best-effort.
|
||||
// If WESL is unavailable the real user still gets Explorer (no custom shell
|
||||
// for their SID). Intentional: don't fail teardown over a missing WMI class.
|
||||
// Remove sm-bootstrap custom shell entry + disable Shell Launcher's per-user entry.
|
||||
await Ps(
|
||||
"$c='root\\\\standardcimv2\\\\embedded';" +
|
||||
@@ -15,32 +16,29 @@ public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService
|
||||
"Invoke-CimMethod -InputObject $w -MethodName RemoveCustomShell -Arguments @{Sid=$sid} -EA SilentlyContinue | Out-Null;" +
|
||||
"Invoke-CimMethod -InputObject $w -MethodName SetEnabled -Arguments @{Enabled=$false} -EA SilentlyContinue | Out-Null" +
|
||||
"}",
|
||||
"Revert Shell Launcher", ct);
|
||||
ct);
|
||||
// Revert escape policies set by Configure-Kiosk.ps1.
|
||||
await Ps(
|
||||
"$s='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System';" +
|
||||
"Remove-ItemProperty $s -Name DisableTaskMgr,DisableLockWorkstation,HideFastUserSwitching -EA SilentlyContinue",
|
||||
"Revert escape policies", ct);
|
||||
ct);
|
||||
}
|
||||
|
||||
// Teardown is BEST-EFFORT (unlike Account/BitLocker which are strict): the answer file's
|
||||
// AutoLogon LogonCount=1 already neutralises auto-logon after the first logon (Windows clears
|
||||
// AutoAdminLogon itself), so these Winlogon cleanups must not fail the whole apply. The op that
|
||||
// matters — removing the sm-bootstrap account — runs regardless and is tolerant too.
|
||||
public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default)
|
||||
{
|
||||
const string key = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'";
|
||||
// DefaultPassword may legitimately be absent → keep its per-cmdlet -EA SilentlyContinue.
|
||||
await Ps($"Set-ItemProperty {key} -Name AutoAdminLogon -Value 0; " +
|
||||
$"Remove-ItemProperty {key} -Name DefaultPassword -EA SilentlyContinue", "Disable auto-logon", ct);
|
||||
const string w = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'";
|
||||
await Ps($"Set-ItemProperty -Path {w} -Name AutoAdminLogon -Value '0' -EA SilentlyContinue; " +
|
||||
$"Remove-ItemProperty -Path {w} -Name DefaultPassword -EA SilentlyContinue; " +
|
||||
$"Remove-ItemProperty -Path {w} -Name DefaultUserName -EA SilentlyContinue; " +
|
||||
$"Remove-ItemProperty -Path {w} -Name DefaultDomainName -EA SilentlyContinue", ct);
|
||||
var u = Esc(bootstrapUser);
|
||||
await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", "Remove bootstrap account", ct);
|
||||
await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct);
|
||||
}
|
||||
private static string Esc(string s) => s.Replace("'", "''");
|
||||
|
||||
// $ErrorActionPreference='Stop' surfaces unexpected hard errors (e.g. a bad registry path);
|
||||
// the intentional per-cmdlet -EA SilentlyContinue above still overrides it for the known
|
||||
// best-effort cleanups.
|
||||
private async Task Ps(string s, string operation, CancellationToken ct)
|
||||
{
|
||||
var r = await runner.RunAsync("powershell.exe",
|
||||
$"-NoProfile -ExecutionPolicy Bypass -Command \"$ErrorActionPreference='Stop'; {s}\"", ct);
|
||||
r.EnsureSuccess(operation);
|
||||
}
|
||||
private Task Ps(string s, CancellationToken ct) =>
|
||||
runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{s}\"", ct);
|
||||
}
|
||||
|
||||
@@ -33,12 +33,9 @@ public class ApplyServicesTests
|
||||
new BitLockerService(Fail().Object).EnableAsync("123456"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BootstrapService_throws_on_nonzero_exit()
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
new BootstrapService(Fail().Object).TearDownAsync("sm-bootstrap"));
|
||||
}
|
||||
// Note: BootstrapService is intentionally best-effort (teardown cleanups must not fail the
|
||||
// apply — auto-logon is already neutralised by the answer file's LogonCount=1), so it does
|
||||
// NOT throw on a non-zero exit.
|
||||
|
||||
[Fact]
|
||||
public async Task AccountService_creates_standard_daily_and_admin()
|
||||
@@ -81,6 +78,9 @@ public class ApplyServicesTests
|
||||
// Removes any TPM-only protector so the device requires the PIN at pre-boot.
|
||||
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
|
||||
s.Contains("Remove-BitLockerKeyProtector")), It.IsAny<CancellationToken>()));
|
||||
// Ejects optical install media first (BitLocker refuses to enroll with bootable media present).
|
||||
run.Verify(r => r.RunAsync("powershell.exe", It.Is<string>(s =>
|
||||
s.Contains("Shell.Application") && s.Contains("Eject")), It.IsAny<CancellationToken>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -20,10 +20,13 @@ public class BootstrapServiceRevertKioskTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RevertKioskAsync_throws_on_nonzero_exit()
|
||||
public async Task RevertKioskAsync_is_best_effort_and_does_not_throw_on_nonzero_exit()
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
// Kiosk revert is best-effort (like TearDownAsync): a non-zero exit must NOT
|
||||
// fail the apply — the real user still gets Explorer regardless of WESL state.
|
||||
var ex = await Record.ExceptionAsync(() =>
|
||||
new BootstrapService(Fail().Object).RevertKioskAsync());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user