Independent review in the win11 VM found four things worth fixing before the fleet sync: - The task action ran powershell -WindowStyle Hidden, which on Win11 (Windows Terminal as console host) flashes a window and steals foreground for ~0.5 s on every fire. Action is now conhost.exe --headless powershell.exe ..., verified silent. - If the share copy of Map-SfldShare.ps1 changes without the manifest DetectionValue being bumped, the engine calls the registrar every cycle. The registrar now takes an idempotent fast path (staged hash matches, task present with our action -> re-stamp and exit, no task churn) and logs one WARN per hash when -ExpectedHash disagrees with the share file. - Replacing a wrong or unreadable mapping now pre-flights the new mapping on a spare letter and keeps the old S: when that fails (except on 1219, where the old session is what blocks the new one). - Credential Manager is re-seeded whenever the HKLM credential changes (password rotation) and after every repair, instead of once per profile. Set-State no longer wipes the stored hash on suspect writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011boHjE3EeexwszdiKDZ7iX
178 lines
8.7 KiB
PowerShell
178 lines
8.7 KiB
PowerShell
# 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, ...).
|
|
#
|
|
# 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',
|
|
# SHA256 the manifest entry detects on. When it differs from the share
|
|
# file the manifest was not bumped with the script; warn loudly once.
|
|
[string]$ExpectedHash = ''
|
|
)
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
|
|
$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
|
|
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
|
Write-Host $line
|
|
}
|
|
|
|
# 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 (-not (Test-Path $src)) { Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"; exit 1 }
|
|
$srcHash = (Get-FileHash -Path $src -Algorithm SHA256).Hash
|
|
$stampKey = 'HKLM:\SOFTWARE\GE\SfldShare'
|
|
|
|
# Manifest / share drift guard. The GE-Enforce entry detects on
|
|
# MapperHash == DetectionValue, so a share edit without a DetectionValue bump
|
|
# makes the engine call this every cycle. Say so once per hash, and take the
|
|
# fast path below so that loop costs nothing and never churns the task.
|
|
if ($ExpectedHash -and ($ExpectedHash -ne $srcHash)) {
|
|
$warned = (Get-ItemProperty -Path $stampKey -Name DriftWarnedHash -EA SilentlyContinue).DriftWarnedHash
|
|
if ($warned -ne $srcHash) {
|
|
Write-RegLog "WARN: share Map-SfldShare.ps1 is $srcHash but the manifest DetectionValue is $ExpectedHash - bump DetectionValue in common/manifest.json or the engine re-runs this every cycle"
|
|
if (-not (Test-Path $stampKey)) { New-Item -Path $stampKey -Force | Out-Null }
|
|
Set-ItemProperty -Path $stampKey -Name DriftWarnedHash -Value $srcHash -Type String -EA SilentlyContinue
|
|
}
|
|
}
|
|
|
|
# Fast path: staged copy already matches the share and the task exists with
|
|
# our action -> nothing to (re)do. Re-stamp and leave (no Register -Force,
|
|
# so a live task is never torn down for no reason).
|
|
$taskNow = Get-ScheduledTask -TaskName $taskName -EA SilentlyContinue
|
|
if ($taskNow -and (Test-Path $mapScript) -and
|
|
((Get-FileHash -Path $mapScript -Algorithm SHA256).Hash -eq $srcHash) -and
|
|
($taskNow.Actions[0].Execute -like '*conhost.exe') -and
|
|
($taskNow.Actions[0].Arguments -like "*$mapScript*")) {
|
|
if (-not (Test-Path $stampKey)) { New-Item -Path $stampKey -Force | Out-Null }
|
|
Set-ItemProperty -Path $stampKey -Name MapperHash -Value $srcHash -Type String -EA SilentlyContinue
|
|
exit 0
|
|
}
|
|
|
|
Write-RegLog '=== Register-MapSfldShare start ==='
|
|
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'"
|
|
}
|
|
|
|
# --- task ------------------------------------------------------------------
|
|
try {
|
|
# conhost --headless, not powershell -WindowStyle Hidden: on Win11 the
|
|
# default console host is Windows Terminal, which ignores the hosted
|
|
# app's Hidden style and flashes a window + steals foreground for
|
|
# ~0.5 s on every fire (measured in the VM). Headless conhost shows
|
|
# nothing and moves focus nowhere.
|
|
$ps = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
$action = New-ScheduledTaskAction -Execute "$env:SystemRoot\System32\conhost.exe" `
|
|
-Argument ('--headless "{0}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "{1}"' -f $ps, $mapScript)
|
|
|
|
$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)
|
|
}
|
|
|
|
# 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 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
|