fix(shopfloor): self-healing S: drive mapper to stop mid-shift drive loss
S: was mapped once per logon (HKLM\Run -> Map-SfldShare.ps1) with no re-check, so any SMB session break the redirector could not silently re-authenticate left S: dead until the user logged out and back in. Replace the Run entry with a scheduled task (BUILTIN\Users, Limited) that fires at logon, every 3 min, on unlock/console-connect, and on network-connect. The mapper now probes S: with a real timed directory read (not net use output parsing, which reports a dead drive as healthy), only repairs after two consecutive failed probes with TCP 445 reachable, and never touches a working drive. Credentials go through New-SmbMapping / CredWrite instead of the net use command line, and a Domain Password credential is seeded so Windows' own reconnect and UNC access share one identity (kills the 1219 conflict class). Register-MapSfldShare stamps HKLM\SOFTWARE\GE\SfldShare\MapperHash with the staged script's SHA256 after success, so GE-Enforce can (re)register it fleet-wide via a common manifest entry and re-fire on version bumps. Verified in the win11 VM: healthy no-op with an open handle held across runs, repair of deleted / wrong-target / present-but-unreadable drives, unaided recovery after a NIC bounce and on the 3-min interval, and the full SYSTEM-from-share registrar path flipping detection to installed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011boHjE3EeexwszdiKDZ7iX
This commit is contained in:
@@ -1,67 +1,30 @@
|
||||
# Register-MapSfldShare.ps1 - Stage Map-SfldShare.ps1 + register an
|
||||
# HKLM\Run entry that maps S: for any interactive user (SupportUser,
|
||||
# ShopFloor, any future end-user accounts).
|
||||
# Register-MapSfldShare.ps1 - stage Map-SfldShare.ps1 and register the
|
||||
# 'GE Map SFLD Share' scheduled task that keeps S: alive for every
|
||||
# interactive BUILTIN\Users member (ShopFloor, SupportUser, ...).
|
||||
#
|
||||
# Why HKLM\Run instead of a scheduled task: Run fires at Explorer
|
||||
# startup in the logged-in user's interactive session with their full
|
||||
# token + HKCU mounted. No principal/LogonType/group-SID plumbing, no
|
||||
# "task fires in session 0 but drive not visible to Explorer" class of
|
||||
# bugs. Works for every BUILTIN\Users member with no extra logic.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine (needs admin) before net
|
||||
# use. ShopFloor is non-admin, so the cred-store fails and net use has
|
||||
# no auth. Our Map-SfldShare.ps1 reads HKLM creds directly and passes
|
||||
# them inline to net use /user: -- no Credential Manager needed.
|
||||
# Replaces the HKLM\Run entry of the same name. Run fired once per logon and
|
||||
# had no way to repair S: mid-shift; a task can fire at logon, on an
|
||||
# interval, on unlock / console connect and when the network comes up.
|
||||
# Principal is the Users GROUP with RunLevel Limited, so it runs in each
|
||||
# logged-on member's own session with a non-elevated token: the mapping
|
||||
# lands where Explorer and the apps look for it.
|
||||
|
||||
param(
|
||||
[int]$IntervalMinutes = 3,
|
||||
[string]$InstallRoot = 'C:\Program Files\GE\SfldShare'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# PC types that are self-contained and must NOT map S:. A Display kiosk is
|
||||
# Entra-joined with local accounts and no SFLD credentials, so the mapping can
|
||||
# only ever fail - once per logon, forever.
|
||||
#
|
||||
# Run-ShopfloorSetup.ps1 already gates the CALL to this script on the same list,
|
||||
# but on 579C144 (2026-08-06) the Run entry was registered on a Display anyway:
|
||||
#
|
||||
# [15:07:55] Set HKLM:\...\Run\GE Map SFLD Share = ...Map-SfldShare.ps1
|
||||
#
|
||||
# with no "Skipping S: drive logon mapper" in the log, so something in the
|
||||
# finalization phase reaches this script past the call-site gate. Gating here as
|
||||
# well makes the outcome correct regardless of who invokes it.
|
||||
$selfContainedTypes = @('Display', 'gea-shopfloor-display')
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$pcType = ''
|
||||
if (Test-Path -LiteralPath $pcTypeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $pcTypeFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($pcType) { $pcType = $pcType.Trim() }
|
||||
}
|
||||
if ($pcType -and ($selfContainedTypes -contains $pcType)) {
|
||||
$d = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force -EA SilentlyContinue | Out-Null }
|
||||
Add-Content -Path (Join-Path $d 'register-mapshare.log') -EA SilentlyContinue `
|
||||
-Value ("[{0}] [INFO] PC type '{1}' is self-contained - not registering the S: mapper." -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $pcType)
|
||||
Write-Host "PC type '$pcType' is self-contained - skipping S: drive mapper."
|
||||
# Remove a stale entry from an earlier image or an earlier code path.
|
||||
try {
|
||||
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
if ((Get-ItemProperty -Path $runKey -Name 'GE Map SFLD Share' -EA SilentlyContinue)) {
|
||||
Remove-ItemProperty -Path $runKey -Name 'GE Map SFLD Share' -Force -EA Stop
|
||||
Write-Host " removed stale 'GE Map SFLD Share' Run entry."
|
||||
}
|
||||
} catch { }
|
||||
return
|
||||
}
|
||||
|
||||
$installRoot = 'C:\Program Files\GE\SfldShare'
|
||||
$mapScript = Join-Path $installRoot 'Map-SfldShare.ps1'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
$logFile = Join-Path $logDir 'register-mapshare.log'
|
||||
$taskName = 'GE Map SFLD Share'
|
||||
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
$runValue = 'GE Map SFLD Share'
|
||||
$legacyTask = 'GE Shopfloor Map S: Drive'
|
||||
$mapScript = Join-Path $InstallRoot 'Map-SfldShare.ps1'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
$logFile = Join-Path $logDir 'register-mapshare.log'
|
||||
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
@@ -71,45 +34,108 @@ function Write-RegLog {
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare start ==='
|
||||
|
||||
# Stage our Map-SfldShare.ps1 to a persistent location
|
||||
if (-not (Test-Path $installRoot)) {
|
||||
New-Item -Path $installRoot -ItemType Directory -Force | Out-Null
|
||||
# Self-contained PC types never get S: (Display kiosk: local accounts, no
|
||||
# SFLD creds, mapping could only fail). Gate here as well as at the call
|
||||
# site - see the 579C144 note in the previous version.
|
||||
$selfContainedTypes = @('Display', 'gea-shopfloor-display')
|
||||
$pcType = ''
|
||||
if (Test-Path -LiteralPath 'C:\Enrollment\pc-type.txt') {
|
||||
$pcType = (Get-Content -LiteralPath 'C:\Enrollment\pc-type.txt' -First 1 -EA SilentlyContinue)
|
||||
if ($pcType) { $pcType = $pcType.Trim() }
|
||||
}
|
||||
if ($pcType -and ($selfContainedTypes -contains $pcType)) {
|
||||
Write-RegLog "PC type '$pcType' is self-contained - not registering the S: mapper."
|
||||
Remove-ItemProperty -Path $runKey -Name $runValue -Force -EA SilentlyContinue
|
||||
if (Get-ScheduledTask -TaskName $taskName -EA SilentlyContinue) { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- stage the script ----------------------------------------------------
|
||||
$src = Join-Path $PSScriptRoot 'lib\Map-SfldShare.ps1'
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination $mapScript -Force
|
||||
Write-RegLog "Staged $src -> $mapScript"
|
||||
} else {
|
||||
Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"
|
||||
exit 1
|
||||
if (-not (Test-Path $src)) { Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"; exit 1 }
|
||||
if (-not (Test-Path $InstallRoot)) { New-Item -Path $InstallRoot -ItemType Directory -Force | Out-Null }
|
||||
Copy-Item -Path $src -Destination $mapScript -Force
|
||||
Write-RegLog "Staged $src -> $mapScript"
|
||||
|
||||
# --- retire the older mechanisms -----------------------------------------
|
||||
if (Get-ItemProperty -Path $runKey -Name $runValue -EA SilentlyContinue) {
|
||||
Remove-ItemProperty -Path $runKey -Name $runValue -Force -EA SilentlyContinue
|
||||
Write-RegLog "Removed HKLM\Run '$runValue' (superseded by the scheduled task)"
|
||||
}
|
||||
if (Get-ScheduledTask -TaskName $legacyTask -EA SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $legacyTask -Confirm:$false -EA SilentlyContinue
|
||||
Write-RegLog "Removed legacy task '$legacyTask'"
|
||||
}
|
||||
|
||||
# Remove the legacy scheduled task if it exists (left behind by older
|
||||
# imaging runs that used the scheduled-task approach).
|
||||
if (Get-ScheduledTask -TaskName $legacyTask -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $legacyTask -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Removed legacy scheduled task '$legacyTask'"
|
||||
} catch {
|
||||
Write-RegLog "Failed to remove legacy task '$legacyTask': $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Register HKLM\Run entry. Runs at Explorer startup for every
|
||||
# interactive user in that user's session.
|
||||
# --- task ------------------------------------------------------------------
|
||||
try {
|
||||
$command = '"{0}" -NoProfile -ExecutionPolicy Bypass -File "{1}"' -f `
|
||||
"$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe", $mapScript
|
||||
$ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
$action = New-ScheduledTaskAction -Execute $ps `
|
||||
-Argument ('-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $mapScript)
|
||||
|
||||
if (-not (Test-Path $runKey)) {
|
||||
New-Item -Path $runKey -Force | Out-Null
|
||||
$ns = 'Root/Microsoft/Windows/TaskScheduler'
|
||||
$triggers = New-Object System.Collections.ArrayList
|
||||
|
||||
# 1. logon (any user)
|
||||
[void]$triggers.Add((New-ScheduledTaskTrigger -AtLogOn))
|
||||
|
||||
# 2. interval, forever
|
||||
$rep = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes)
|
||||
[void]$triggers.Add($rep)
|
||||
|
||||
# 3. console connect (1) + session unlock (8)
|
||||
$ssCls = Get-CimClass -ClassName MSFT_TaskSessionStateChangeTrigger -Namespace $ns
|
||||
foreach ($state in 1, 8) {
|
||||
$t = New-CimInstance -CimClass $ssCls -ClientOnly
|
||||
$t.StateChange = $state
|
||||
$t.Delay = 'PT10S'
|
||||
[void]$triggers.Add($t)
|
||||
}
|
||||
New-ItemProperty -Path $runKey -Name $runValue -Value $command -PropertyType String -Force | Out-Null
|
||||
Write-RegLog "Set $runKey\$runValue = $command"
|
||||
|
||||
# 4. network profile connected (NetworkProfile/Operational 10000) - wifi
|
||||
# roam / cable back / VPN up. Short delay so DNS is usable.
|
||||
$evCls = Get-CimClass -ClassName MSFT_TaskEventTrigger -Namespace $ns
|
||||
$ev = New-CimInstance -CimClass $evCls -ClientOnly
|
||||
$ev.Subscription = '<QueryList><Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational"><Select Path="Microsoft-Windows-NetworkProfile/Operational">*[System[(EventID=10000)]]</Select></Query></QueryList>'
|
||||
$ev.Delay = 'PT15S'
|
||||
[void]$triggers.Add($ev)
|
||||
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-MultipleInstances IgnoreNew `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
|
||||
-StartWhenAvailable `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-Hidden
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName `
|
||||
-Action $action -Trigger $triggers.ToArray() -Principal $principal -Settings $settings `
|
||||
-Force -ErrorAction Stop | Out-Null
|
||||
|
||||
Write-RegLog "Registered task '$taskName' (BUILTIN\Users, Limited; logon + every $IntervalMinutes min + unlock/connect + network-up)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register Run key: $_"
|
||||
Write-RegLog "FAILED to register task: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- stamp -----------------------------------------------------------------
|
||||
# Written only after staging + registration both succeeded. The GE-Enforce
|
||||
# manifest entry detects on MapperHash == SHA256 of Map-SfldShare.ps1, so a
|
||||
# new script version (new hash) or a failed registration (no stamp) re-fires
|
||||
# this registrar on the next cycle.
|
||||
try {
|
||||
$hash = (Get-FileHash -Path $mapScript -Algorithm SHA256).Hash
|
||||
$stampKey = 'HKLM:\SOFTWARE\GE\SfldShare'
|
||||
if (-not (Test-Path $stampKey)) { New-Item -Path $stampKey -Force | Out-Null }
|
||||
Set-ItemProperty -Path $stampKey -Name MapperHash -Value $hash -Type String
|
||||
Set-ItemProperty -Path $stampKey -Name RegisteredAt -Value (Get-Date -Format 's') -Type String
|
||||
Set-ItemProperty -Path $stampKey -Name TaskName -Value $taskName -Type String
|
||||
Write-RegLog "Stamped $stampKey MapperHash=$hash"
|
||||
} catch {
|
||||
Write-RegLog "Stamp write failed (registrar will re-run next cycle): $_"
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare end ==='
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user