generalize: folderRedirect -- keep the SID generalized, persist user data on D:
The alternative to keepMachineSid for "survives an OS rebuild". Instead of moving the whole SID-bound profile to D: (which forces a fixed SID), keep /generalize -- so every machine and every rebuild gets its own random SID -- and redirect only the user's data folders (Desktop, Documents, Downloads, ...) to the persistent data volume. Files survive a rebuild; per-user registry settings do not, which is the accepted trade for not touching the SID. Three pieces. A per-user script calls SHSetKnownFolderPath to point each known folder at D:\UserData\<folder>; it is registered through Active Setup, which runs it once per profile at first logon -- including the fresh profile each generalized rebuild creates. And the onstart SYSTEM boot script grants the well-known Users group inheritable full control on the data tree, so the account behind whatever SID this rebuild produced can reach files an earlier SID created. The heal/relocation path is now gated on profilesDirectory, so it and folderRedirect stay mutually exclusive and neither breaks the other's null. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117qMyjpuXsjpVAcpJbFD8g
This commit is contained in:
parent
d3ac2bf475
commit
60013006d6
1 changed files with 84 additions and 22 deletions
|
|
@ -52,6 +52,13 @@ in
|
|||
# Correct only for an image that is always this one machine; a fleet that
|
||||
# deploys the same image to many hosts wants the default generalization.
|
||||
keepMachineSid ? false,
|
||||
# Known-Folder redirection: keep the SID-bound profile on C: (so /generalize
|
||||
# can still randomize the SID per machine) but point the user's data folders
|
||||
# at the persistent data disk, so files -- not per-user registry settings --
|
||||
# survive an OS rebuild. e.g. { base = "D:\\UserData"; folders = [ "Desktop"
|
||||
# "Documents" "Downloads" ]; }. Needs dataDisk to provide the volume, and is
|
||||
# mutually exclusive with profilesDirectory.
|
||||
folderRedirect ? null,
|
||||
}: let
|
||||
# Convert "8e8cd8" hex to "142 140 216" decimal RGB for Windows registry
|
||||
hexToRgbStr = hex: let
|
||||
|
|
@ -194,7 +201,56 @@ in
|
|||
# temporary-profile fallback), put it back: drop the temp key, rename .bak to
|
||||
# the live SID, remove the temp directory, and drop a flag so the caller
|
||||
# knows to reboot.
|
||||
healProfileScript = pkgs.writeText "vmix-heal-profile.ps1" ''
|
||||
# Known-Folder GUIDs for the redirectable user folders.
|
||||
knownFolderGuids = {
|
||||
Desktop = "{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}";
|
||||
Documents = "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}";
|
||||
Downloads = "{374DE290-123F-4565-9164-39C4925E467B}";
|
||||
Pictures = "{33E28130-4E1E-4676-835A-98395C3BC476}";
|
||||
Music = "{4BD8D571-6D19-48D3-BE97-422220080E43}";
|
||||
Videos = "{18989B1D-99B5-455B-841C-AB7C74E4DDFC}";
|
||||
Favorites = "{1777F761-68AD-4D8A-87BD-30B759FA33DD}";
|
||||
};
|
||||
redirectFolders = if folderRedirect != null
|
||||
then (folderRedirect.folders or [ "Desktop" "Documents" "Downloads" "Pictures" "Music" "Videos" ])
|
||||
else [ ];
|
||||
redirectBase = if folderRedirect != null then (folderRedirect.base or "D:\\UserData") else "D:\\UserData";
|
||||
|
||||
# Per-user, run once per profile via Active Setup: point each known folder at
|
||||
# its directory under the data volume. SHSetKnownFolderPath updates both the
|
||||
# registration and the shell-folder registry; it does not move files, so a
|
||||
# freshly created profile's empty C: folder is simply repointed at the D: one,
|
||||
# which already holds this user's accumulated files after a rebuild.
|
||||
folderRedirectPs1 = pkgs.writeText "vmix-redirect-folders.ps1" (lib.optionalString (folderRedirect != null) ''
|
||||
$sig = @'
|
||||
[DllImport("shell32.dll", CharSet=CharSet.Unicode)]
|
||||
public static extern int SHSetKnownFolderPath(ref System.Guid rfid, uint dwFlags, System.IntPtr hToken, string pszPath);
|
||||
'@
|
||||
$kf = Add-Type -MemberDefinition $sig -Name KFP -Namespace Vmix -PassThru
|
||||
$map = @{
|
||||
${lib.concatMapStringsSep "\n " (f: "'${f}' = '${knownFolderGuids.${f}}'") redirectFolders}
|
||||
}
|
||||
foreach ($name in $map.Keys) {
|
||||
$target = Join-Path '${redirectBase}' $name
|
||||
New-Item -ItemType Directory -Force -Path $target -ErrorAction SilentlyContinue | Out-Null
|
||||
$guid = [System.Guid]$map[$name]
|
||||
[void]$kf::SHSetKnownFolderPath([ref]$guid, 0, [System.IntPtr]::Zero, $target)
|
||||
}
|
||||
'');
|
||||
|
||||
# Active Setup fires StubPath once per user at first logon -- including the
|
||||
# fresh profile each generalized rebuild creates -- which is exactly when the
|
||||
# redirection needs re-applying. Backslashes doubled for .reg.
|
||||
activeSetupRegistry = lib.optionalString (folderRedirect != null) ''
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Active Setup\Installed Components\{6f3b8c2a-1d4e-4f9a-b8c1-0a1b2c3d4e5f}]
|
||||
@="vmix folder redirection"
|
||||
"StubPath"="powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\\vmix-redirect-folders.ps1"
|
||||
"Version"="1"
|
||||
'';
|
||||
|
||||
healProfileScript = pkgs.writeText "vmix-heal-profile.ps1" (lib.optionalString (profilesDirectory != null) ''
|
||||
$pl = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
|
||||
$bak = Get-ChildItem $pl -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.PSChildName -like '*.bak' -and
|
||||
|
|
@ -207,29 +263,33 @@ in
|
|||
Remove-Item '${profilesDirectory}\TEMP' -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -Path C:\Windows\Temp\vmix-profile-healed -ItemType File -Force | Out-Null
|
||||
}
|
||||
'';
|
||||
'');
|
||||
|
||||
# Ensures D: exists on every boot and heals a profile that went temporary.
|
||||
#
|
||||
# generalize strips MountedDevices, so the shipped image carries no drive
|
||||
# letter for the data disk, and the specialize pass that would re-assert it
|
||||
# runs only in the build, not on the target. The zvol therefore boots
|
||||
# letterless, the first autologon cannot find its relocated profile at
|
||||
# ${profilesDirectory}\${username} (event 1511) and falls back to a temporary
|
||||
# one, backing the real key up as .bak and making the fault stick.
|
||||
#
|
||||
# diskpart assign writes MountedDevices, so once this has run once D: is
|
||||
# persistent for every later boot. Only the first boot is exposed, and if it
|
||||
# left a .bak the heal puts it back and reboots -- the next boot, D: now
|
||||
# persistent and ProfileList clean, logs straight into the real profile.
|
||||
# One onstart / SYSTEM script for whatever the data disk needs before logon:
|
||||
# assign its letter, and then either heal a relocated profile that went
|
||||
# temporary (profilesDirectory) or make the redirected data folders reachable
|
||||
# by whatever account this rebuild created (folderRedirect). Both cannot apply
|
||||
# at once -- a profile is either wholly on D: or only its data folders are.
|
||||
bootDataProfileScript = pkgs.writeText "vmix-data-profile.cmd" ''
|
||||
@echo off
|
||||
call C:\vmix-init-data-disk.cmd
|
||||
${lib.optionalString (dataDisk != null) "call C:\\vmix-init-data-disk.cmd"}
|
||||
${lib.optionalString (profilesDirectory != null) ''
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File C:\vmix-heal-profile.ps1 > C:\Windows\Temp\vmix-data-profile.log 2>&1
|
||||
if exist C:\Windows\Temp\vmix-profile-healed (
|
||||
del /q C:\Windows\Temp\vmix-profile-healed
|
||||
shutdown /r /t 5 /c "vmix: repaired relocated profile, restarting"
|
||||
)
|
||||
''}
|
||||
${lib.optionalString (folderRedirect != null) ''
|
||||
:: The redirected folders live under a per-user account whose SID changes on
|
||||
:: every generalized rebuild, so grant the well-known Users group -- which
|
||||
:: any account joins and which is SID-stable across machines -- inheritable
|
||||
:: full control, and let the per-user redirect (Active Setup) point the known
|
||||
:: folders here. Runs as SYSTEM, before any logon.
|
||||
if not exist "${redirectBase}" mkdir "${redirectBase}"
|
||||
${lib.concatMapStringsSep "\n " (f: ''if not exist "${redirectBase}\${f}" mkdir "${redirectBase}\${f}"'') redirectFolders}
|
||||
icacls "${redirectBase}" /grant *S-1-5-32-545:(OI)(CI)F /t > C:\Windows\Temp\vmix-redirect-prep.log 2>&1
|
||||
''}
|
||||
'';
|
||||
|
||||
folderLocationsXml = lib.optionalString (profilesDirectory != null) ''
|
||||
|
|
@ -469,7 +529,7 @@ in {
|
|||
# so the profile relocation cannot ride the unattend there. It is written to
|
||||
# the registry offline instead, before the build's OOBE creates the profile,
|
||||
# so the account still lands on the data volume. Empty otherwise.
|
||||
windowsRegistry = profileListRegistry;
|
||||
windowsRegistry = profileListRegistry + activeSetupRegistry;
|
||||
# The blank disk is attached for the Audit Mode boot itself, so the disk-init
|
||||
# command and the profile relocation both happen under OOBE in the build VM.
|
||||
# That is what makes delayOobeRun unnecessary: nothing is left to do on real
|
||||
|
|
@ -479,11 +539,13 @@ in {
|
|||
{ source = oobeXml; dest = "/oobe-unattend.xml"; }
|
||||
{ source = postOobeScript; dest = "/post-oobe.cmd"; }
|
||||
{ source = masScript; dest = "/MAS_AIO.cmd"; }
|
||||
] ++ lib.optionals (dataDisk != null) [
|
||||
{ source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
|
||||
{ source = healProfileScript; dest = "/vmix-heal-profile.ps1"; }
|
||||
{ source = bootDataProfileScript; dest = "/vmix-data-profile.cmd"; }
|
||||
]
|
||||
] ++ lib.optionals (dataDisk != null) (
|
||||
[ { source = initDataDiskScript; dest = "/vmix-init-data-disk.cmd"; }
|
||||
{ source = bootDataProfileScript; dest = "/vmix-data-profile.cmd"; }
|
||||
]
|
||||
++ lib.optional (profilesDirectory != null) { source = healProfileScript; dest = "/vmix-heal-profile.ps1"; }
|
||||
++ lib.optional (folderRedirect != null) { source = folderRedirectPs1; dest = "/vmix-redirect-folders.ps1"; }
|
||||
)
|
||||
++ lib.optional (writeFilter != null) { source = uwfConfigScript; dest = "/vmix-uwf-config.cmd"; }
|
||||
++ lib.optionals (staticIP != null) [
|
||||
{ source = staticIPScript; dest = "/vmix-static-ip.cmd"; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue