diff --git a/playbook/shopfloor-setup/CMM/09-Setup-CMM.ps1 b/playbook/shopfloor-setup/CMM/09-Setup-CMM.ps1 index 2fc4e5d..6ff926b 100644 --- a/playbook/shopfloor-setup/CMM/09-Setup-CMM.ps1 +++ b/playbook/shopfloor-setup/CMM/09-Setup-CMM.ps1 @@ -3,19 +3,20 @@ # At imaging time the tsgwp00525 SFLD share is NOT yet reachable - Azure DSC # has not provisioned the share credentials that early. So we install from a # WinPE-staged local copy at C:\CMM-Install (put there by startnet.cmd when -# the tech picks pc-type=CMM), then register a logon-triggered scheduled -# task that runs CMM-Enforce.ps1 for ongoing updates from the share. +# the tech picks pc-type=CMM). Ongoing enforcement is handled by GE-Enforce +# (registered separately in Run-ShopfloorSetup.ps1) reading cmm/manifest.json +# from the tsgwp00525 share. # # Sequence: # 1. Enable .NET Framework 3.5 (PC-DMIS 2016 prereq on Win10/11 where 3.5 # is an off-by-default optional feature). # 2. Run Install-FromManifest against C:\CMM-Install\cmm-manifest.json. -# 3. Stage Install-FromManifest.ps1 + CMM-Enforce.ps1 + the manifest to -# C:\Program Files\GE\CMM so the scheduled task has them after imaging. -# 4. Register a SYSTEM scheduled task "GE CMM Enforce" that runs -# CMM-Enforce.ps1 on any user logon. -# 5. Delete C:\CMM-Install to reclaim the ~2 GB of bootstrap installers. -# The share-side enforcer takes over from here. +# 2.5. Grant BUILTIN\Users Modify on PC-DMIS install dirs (Hexagon-documented +# approach for non-admin runtime). +# 3. Delete C:\CMM-Install to reclaim the ~2 GB of bootstrap installers. +# +# Library lookup: the imaging-time install uses the common Install-FromManifest +# library at ..\common\lib\Install-FromManifest.ps1 (relative to $PSScriptRoot). # # Log: C:\Logs\CMM\09-Setup-CMM.log (stdout from this script) plus the # install-time log at C:\Logs\CMM\install.log written by Install-FromManifest. @@ -24,13 +25,7 @@ $ErrorActionPreference = 'Continue' $stagingRoot = 'C:\CMM-Install' $stagingMani = Join-Path $stagingRoot 'cmm-manifest.json' -$libSource = Join-Path $PSScriptRoot 'lib\Install-FromManifest.ps1' -$enforceSource = Join-Path $PSScriptRoot 'CMM-Enforce.ps1' - -$runtimeRoot = 'C:\Program Files\GE\CMM' -$runtimeLibDir = Join-Path $runtimeRoot 'lib' -$runtimeLib = Join-Path $runtimeLibDir 'Install-FromManifest.ps1' -$runtimeEnforce = Join-Path $runtimeRoot 'CMM-Enforce.ps1' +$libSource = Join-Path $PSScriptRoot '..\common\lib\Install-FromManifest.ps1' $logDir = 'C:\Logs\CMM' $logFile = Join-Path $logDir 'install.log' @@ -163,65 +158,10 @@ foreach ($dir in $pcdmisDirs) { } # ============================================================================ -# Step 3: Stage runtime scripts to C:\Program Files\GE\CMM +# Step 3: Clean up the bootstrap staging dir # ============================================================================ -# These files survive past the bootstrap cleanup so the logon-triggered -# scheduled task can run them. The manifest is staged as well so the enforcer -# has a fallback in case the share copy is unreachable on first logon. -Write-CMMLog "Staging runtime scripts to $runtimeRoot" -foreach ($dir in @($runtimeRoot, $runtimeLibDir)) { - if (-not (Test-Path $dir)) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null - } -} -Copy-Item -Path $libSource -Destination $runtimeLib -Force -Copy-Item -Path $enforceSource -Destination $runtimeEnforce -Force - -# ============================================================================ -# Step 4: Register "GE CMM Enforce" scheduled task (logon trigger, SYSTEM) -# ============================================================================ -$taskName = 'GE CMM Enforce' - -# Drop any stale version first so re-imaging is idempotent. -$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue -if ($existing) { - Write-CMMLog "Removing existing scheduled task '$taskName'" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} - -Write-CMMLog "Registering scheduled task '$taskName' (logon trigger, SYSTEM)" -try { - $action = New-ScheduledTaskAction ` - -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runtimeEnforce`"" - - $trigger = New-ScheduledTaskTrigger -AtLogOn - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Hours 2) ` - -MultipleInstances IgnoreNew - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'GE CMM: enforce Hexagon apps against tsgwp00525 SFLD share on user logon' | Out-Null - - Write-CMMLog "Scheduled task registered" -} catch { - Write-CMMLog "Failed to register scheduled task: $_" "ERROR" -} - -# ============================================================================ -# Step 5: Clean up the bootstrap staging dir -# ============================================================================ -# ~2 GB reclaimed. From here on, CMM-Enforce.ps1 runs against the tsgwp00525 -# share, which is the canonical source for ongoing updates. +# ~2 GB reclaimed. From here on, GE-Enforce takes over from the tsgwp00525 +# share for ongoing updates. if (Test-Path $stagingRoot) { Write-CMMLog "Deleting bootstrap staging at $stagingRoot" try { diff --git a/playbook/shopfloor-setup/CMM/CMM-Enforce.ps1 b/playbook/shopfloor-setup/CMM/CMM-Enforce.ps1 deleted file mode 100644 index e457a5f..0000000 --- a/playbook/shopfloor-setup/CMM/CMM-Enforce.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -# CMM-Enforce.ps1 - On-logon CMM app enforcer (the mini-DSC side). -# -# Runs under a SYSTEM scheduled task triggered at user logon. Mounts the -# tsgwp00525 SFLD share using creds written to HKLM by Azure DSC, reads -# cmm-manifest.json from the share, and hands off to Install-FromManifest.ps1 -# which installs anything whose detection fails. -# -# Why logon trigger: shopfloor operators log in at shift start; the PC may -# have been off or DSC may not have provisioned the SFLD creds until the -# Intune side ran post-PPKG. Logon is a natural catch-up point. Once detection -# passes for every app, each run is ~seconds of no-ops. -# -# Why SYSTEM: installers need machine-wide rights and registry access. The -# task is triggered by logon but runs outside the user's session. -# -# Graceful degradation: -# - SFLD creds missing (Azure DSC hasn't run yet) -> log + exit 0 -# - Share unreachable (network, VPN) -> log + exit 0 -# - Install failure on any one app -> log + continue with the rest -# -# Never returns non-zero to the task scheduler; failures show up in the log. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\CMM' -$libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$logDir = 'C:\Logs\CMM' -$logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) -$driveLetter = 'S:' - -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-EnforceLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue -} - -Write-EnforceLog "================================================================" -Write-EnforceLog "=== CMM-Enforce session start (PID $PID, user $env:USERNAME) ===" -Write-EnforceLog "================================================================" - -# --- Load site-config + pcProfile (for cmmSharePath) --- -# Dot-source the same Get-PCProfile.ps1 used during imaging. It walks -# C:\Enrollment\site-config.json into $pcProfile/$siteConfig script variables. -$getProfileScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Get-PCProfile.ps1' -if (-not (Test-Path $getProfileScript)) { - Write-EnforceLog "Get-PCProfile.ps1 not found at $getProfileScript - is this a CMM PC?" "ERROR" - exit 0 -} -. $getProfileScript - -if (-not $pcProfile -or -not $pcProfile.cmmSharePath) { - Write-EnforceLog "No cmmSharePath in profile - nothing to enforce" "WARN" - exit 0 -} - -$sharePath = $pcProfile.cmmSharePath -Write-EnforceLog "Share: $sharePath" - -# --- SFLD credential lookup (written by Azure DSC post-PPKG). Bail gracefully -# if the creds haven't been provisioned yet - next logon will retry. --- -function Get-SFLDCredential { - param([string]$ServerName) - $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' - if (-not (Test-Path $basePath)) { return $null } - - foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if (-not $props -or -not $props.TargetHost) { continue } - if ($props.TargetHost -eq $ServerName -or - $props.TargetHost -like "$ServerName.*" -or - $ServerName -like "$($props.TargetHost).*") { - return @{ - Username = $props.Username - Password = $props.Password - TargetHost = $props.TargetHost - KeyName = $entry.PSChildName - } - } - } - return $null -} - -$serverName = ($sharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1 -$cred = Get-SFLDCredential -ServerName $serverName - -if (-not $cred -or -not $cred.Username -or -not $cred.Password) { - Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry at next logon" - exit 0 -} - -Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" - -# --- Mount the share --- -net use $driveLetter /delete /y 2>$null | Out-Null - -$netResult = & net use $driveLetter $sharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" "WARN" - Write-EnforceLog "Share unreachable - probably off-network. Will retry at next logon." - exit 0 -} -Write-EnforceLog "Mounted $sharePath as $driveLetter" - -try { - $manifestOnShare = Join-Path $driveLetter 'cmm-manifest.json' - if (-not (Test-Path $manifestOnShare)) { - Write-EnforceLog "cmm-manifest.json not found on share - nothing to enforce" "WARN" - return - } - - if (-not (Test-Path $libPath)) { - Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" "ERROR" - return - } - - Write-EnforceLog "Handing off to Install-FromManifest.ps1 (InstallerRoot=$driveLetter)" - & $libPath -ManifestPath $manifestOnShare -InstallerRoot $driveLetter -LogFile $logFile - $rc = $LASTEXITCODE - Write-EnforceLog "Install-FromManifest returned $rc" -} -finally { - net use $driveLetter /delete /y 2>$null | Out-Null - Write-EnforceLog "Unmounted $driveLetter" - Write-EnforceLog "=== CMM-Enforce session end ===" -} - -# Always return 0 so the scheduled task never shows "last run failed" noise. -exit 0 diff --git a/playbook/shopfloor-setup/CMM/cmm-manifest.json b/playbook/shopfloor-setup/CMM/cmm-manifest.json index e0a295c..4a12939 100644 --- a/playbook/shopfloor-setup/CMM/cmm-manifest.json +++ b/playbook/shopfloor-setup/CMM/cmm-manifest.json @@ -1,6 +1,6 @@ { "Version": "2.0", - "_comment": "CMM machine-app manifest. Consumed by both 09-Setup-CMM.ps1 (at imaging time, reading from C:\\CMM-Install\\) and CMM-Enforce.ps1 (on logon, reading from the tsgwp00525 share). Option 3 (patched-MSI) install strategy: we bypass Hexagon's Burn bundle entirely for PC-DMIS 2016 and 2019 R2. The main PC-DMIS MSIs have been patched via COM SQL UPDATE (msibuild-style) to force the Condition column to '0' for two custom actions: ProcessLicensingFromBundle (which would otherwise spin for ~13 minutes trying to activate against licensing.wilcoxassoc.com with empty credentials) and IsLicenseDateValid (which would fail the install with 'no valid license'). With both CAs disabled, the MSI installs cleanly with no license present; PCDLRN.exe installs and loads at runtime and the tech activates a real license via clmadmin.exe after imaging. VS 2010/2012 x64 runtime prereqs are handled by the shared preinstall.json VC++ x64 entries (which run before this manifest). CLM Tools 1.5/1.7 chained MSIs from the original bundles are intentionally SKIPPED; CLM 1.8.73 standalone provides the admin + runtime interfaces. Protect Viewer is kept because it's useful alongside PC-DMIS 2019 R2.", + "_comment": "CMM machine-app manifest, imaging-time only. Consumed by 09-Setup-CMM.ps1 reading from C:\\CMM-Install\\. Ongoing enforcement is handled separately by GE-Enforce reading cmm/manifest.json from the tsgwp00525 share. Option 3 (patched-MSI) install strategy: we bypass Hexagon's Burn bundle entirely for PC-DMIS 2016 and 2019 R2. The main PC-DMIS MSIs have been patched via COM SQL UPDATE (msibuild-style) to force the Condition column to '0' for two custom actions: ProcessLicensingFromBundle (which would otherwise spin for ~13 minutes trying to activate against licensing.wilcoxassoc.com with empty credentials) and IsLicenseDateValid (which would fail the install with 'no valid license'). With both CAs disabled, the MSI installs cleanly with no license present; PCDLRN.exe installs and loads at runtime and the tech activates a real license via clmadmin.exe after imaging. VS 2010/2012 x64 runtime prereqs are handled by the shared preinstall.json VC++ x64 entries (which run before this manifest). CLM Tools 1.5/1.7 chained MSIs from the original bundles are intentionally SKIPPED; CLM 1.8.73 standalone provides the admin + runtime interfaces. Protect Viewer is kept because it's useful alongside PC-DMIS 2019 R2.", "Applications": [ { "_comment": "PC-DMIS 2016 main MSI (PATCHED). ProcessLicensingFromBundle + IsLicenseDateValid custom actions have been pre-disabled by SQL UPDATE of InstallExecuteSequence.Condition to '0'. Install args: INSTALLFOLDER/APPLICATIONFOLDER paths have embedded double quotes to survive the runner's command-line concatenation when the path contains spaces. USINGWPFINSTALLER=1 mirrors the Burn bundle default and ensures HandleLicenseChoice CA (seq 783) stays skipped. HEIP=0 disables Hexagon telemetry. INSTALLPDFCONVERTER=0 skips the Nitro PDF converter. The patched MSI has a HashMismatch signature, which is expected and accepted by Windows Installer in /qn mode.", diff --git a/playbook/shopfloor-setup/CMM/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/CMM/lib/Install-FromManifest.ps1 deleted file mode 100644 index fcea812..0000000 --- a/playbook/shopfloor-setup/CMM/lib/Install-FromManifest.ps1 +++ /dev/null @@ -1,300 +0,0 @@ -# Install-FromManifest.ps1 - Generic JSON-manifest installer for CMM apps. -# -# Duplicated (by design, for now) from Shopfloor\00-PreInstall-MachineApps.ps1 -# with the Standard-PC-specific bits stripped out: -# - no PCTypes filter (every CMM manifest entry is CMM-only) -# - no site-name / machine-number placeholder replacement -# - no KillAfterDetection shortcut (Hexagon Burn bundles exit cleanly) -# -# A future pass will unify both runners behind one library; keeping them -# separate now avoids touching the Standard PC imaging path. -# -# Called from: -# - 09-Setup-CMM.ps1 at imaging time with InstallerRoot=C:\CMM-Install -# - CMM-Enforce.ps1 on logon with InstallerRoot= -# -# Returns via exit code: 0 if every required app is either already installed -# or installed successfully; non-zero if any install failed. - -param( - [Parameter(Mandatory=$true)] - [string]$ManifestPath, - - [Parameter(Mandatory=$true)] - [string]$InstallerRoot, - - [Parameter(Mandatory=$true)] - [string]$LogFile -) - -$ErrorActionPreference = 'Continue' - -$logDir = Split-Path -Parent $LogFile -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-InstallLog { - param( - [Parameter(Mandatory=$true, Position=0)] - [string]$Message, - [Parameter(Position=1)] - [ValidateSet('INFO','WARN','ERROR')] - [string]$Level = 'INFO' - ) - $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "[$stamp] [$Level] $Message" - Write-Host $line - - # Synchronous write-through so each line hits disk immediately, mirroring - # the preinstall runner's approach - protects forensic trail if an installer - # triggers a reboot mid-loop. - try { - $fs = New-Object System.IO.FileStream( - $LogFile, - [System.IO.FileMode]::Append, - [System.IO.FileAccess]::Write, - [System.IO.FileShare]::Read, - 4096, - [System.IO.FileOptions]::WriteThrough - ) - $bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n") - $fs.Write($bytes, 0, $bytes.Length) - $fs.Flush() - $fs.Dispose() - } catch { - Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue - } -} - -Write-InstallLog "================================================================" -Write-InstallLog "=== Install-FromManifest session start (PID $PID) ===" -Write-InstallLog "Manifest: $ManifestPath" -Write-InstallLog "InstallerRoot: $InstallerRoot" -Write-InstallLog "================================================================" - -if (-not (Test-Path -LiteralPath $ManifestPath)) { - Write-InstallLog "Manifest not found: $ManifestPath" "ERROR" - exit 2 -} - -if (-not (Test-Path -LiteralPath $InstallerRoot)) { - Write-InstallLog "InstallerRoot not found: $InstallerRoot" "ERROR" - exit 2 -} - -try { - $config = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -} catch { - Write-InstallLog "Failed to parse manifest: $_" "ERROR" - exit 2 -} - -if (-not $config.Applications) { - Write-InstallLog "No Applications in manifest - nothing to do" - exit 0 -} - -Write-InstallLog "Manifest lists $($config.Applications.Count) app(s)" - -# Detection helper - mirrors the preinstall runner's logic. Registry path + -# optional value name + optional exact value. The exact-value compare is how -# version-pinned drift detection works: bumping DetectionValue in the manifest -# makes the current install "fail" detection and reinstall. -function Test-AppInstalled { - param($App) - - if (-not $App.DetectionMethod) { return $false } - - try { - switch ($App.DetectionMethod) { - "Registry" { - if (-not (Test-Path $App.DetectionPath)) { return $false } - if ($App.DetectionName) { - $value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue - if (-not $value) { return $false } - if ($App.DetectionValue) { - return ($value.$($App.DetectionName) -eq $App.DetectionValue) - } - return $true - } - return $true - } - "File" { - return Test-Path $App.DetectionPath - } - "FileVersion" { - # Compare a file's VersionInfo.FileVersion against the - # manifest's expected value. Used for version-pinned MSI/EXE - # installs where existence alone doesn't tell you whether - # the right release is on disk. Exact string match - the - # manifest must carry the exact version the vendor stamps - # into the binary. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " FileVersion detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-Item $App.DetectionPath -ErrorAction Stop).VersionInfo.FileVersion - if (-not $actual) { return $false } - return ($actual -eq $App.DetectionValue) - } - "Hash" { - # Compare SHA256 of the on-disk file against the manifest's - # expected value. Used for content-versioned files that do not - # expose a DisplayVersion (secrets like eMxInfo.txt). Bumping - # DetectionValue in the manifest and replacing the file on the - # share is the entire update workflow. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " Hash detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-FileHash -Path $App.DetectionPath -Algorithm SHA256 -ErrorAction Stop).Hash - return ($actual -ieq $App.DetectionValue) - } - default { - Write-InstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN" - return $false - } - } - } catch { - Write-InstallLog " Detection check threw: $_" "WARN" - return $false - } -} - -$installed = 0 -$skipped = 0 -$failed = 0 - -foreach ($app in $config.Applications) { - # Cancel any pending reboot scheduled by a previous installer, same as the - # preinstall runner. Some Burn bundles schedule a reboot even with /norestart - # (chained bootstrapper ignores the flag for some internal prereqs). - cmd /c "shutdown /a 2>nul" *>$null - - Write-InstallLog "==> $($app.Name)" - - if (Test-AppInstalled -App $app) { - Write-InstallLog " Already installed at expected version - skipping" - $skipped++ - continue - } - - $installerPath = Join-Path $InstallerRoot $app.Installer - if (-not (Test-Path -LiteralPath $installerPath)) { - Write-InstallLog " Installer file not found: $installerPath" "ERROR" - $failed++ - continue - } - - Write-InstallLog " Installing from $installerPath" - if ($app.InstallArgs) { - Write-InstallLog " InstallArgs: $($app.InstallArgs)" - } - - try { - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden - - if ($app.Type -eq "MSI") { - $safeName = $app.Name -replace '[^a-zA-Z0-9]','_' - $msiLog = Join-Path $logDir "msi-$safeName.log" - if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue } - - $psi.FileName = "msiexec.exe" - $psi.Arguments = "/i `"$installerPath`"" - if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs } - $psi.Arguments += " /L*v `"$msiLog`"" - Write-InstallLog " msiexec verbose log: $msiLog" - } - elseif ($app.Type -eq "EXE") { - $psi.FileName = $installerPath - if ($app.InstallArgs) { $psi.Arguments = $app.InstallArgs } - if ($app.LogFile) { - Write-InstallLog " Installer log: $($app.LogFile)" - } - } - else { - Write-InstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR" - $failed++ - continue - } - - # Use Process.Start directly rather than Start-Process because PS 5.1's - # Start-Process -PassThru disposes the process handle when control returns, - # making ExitCode read as $null. Direct Process.Start gives us a live handle. - $proc = [System.Diagnostics.Process]::Start($psi) - - # No per-app timeout here - PC-DMIS bundles can run 20+ minutes on slow - # disks and we don't want to kill them mid-chain. The calling script - # controls overall session timing. - $proc.WaitForExit() - $exitCode = $proc.ExitCode - - # Burn and MSI exit codes: - # 0 success - # 1641 success, reboot initiated - # 3010 success, reboot pending - if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010) { - Write-InstallLog " Exit code $exitCode - SUCCESS" - if ($exitCode -eq 3010) { Write-InstallLog " (Reboot pending for $($app.Name))" } - if ($exitCode -eq 1641) { Write-InstallLog " (Installer initiated a reboot for $($app.Name))" } - $installed++ - } - else { - Write-InstallLog " Exit code $exitCode - FAILED" "ERROR" - - if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) { - Write-InstallLog " --- last 30 lines of $($app.LogFile) ---" - Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - Write-InstallLog " --- end installer log tail ---" - } - - if ($app.Type -eq "MSI" -and $msiLog -and (Test-Path $msiLog)) { - Write-InstallLog " --- meaningful lines from $msiLog ---" - $patterns = @( - 'Note: 1: ', - 'return value 3', - 'Error \d+\.', - 'CustomAction .* returned actual error', - 'Failed to ', - 'Installation failed', - '1: 2262', - '1: 2203', - '1: 2330' - ) - $regex = ($patterns -join '|') - $matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue | - Select-Object -First 30 - if ($matches) { - foreach ($m in $matches) { Write-InstallLog " $($m.Line.Trim())" } - } else { - Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - } - Write-InstallLog " --- end MSI log scan ---" - } - - $failed++ - } - } catch { - Write-InstallLog " Install threw: $_" "ERROR" - $failed++ - } -} - -Write-InstallLog "============================================" -Write-InstallLog "Install-FromManifest complete: $installed installed, $skipped skipped, $failed failed" -Write-InstallLog "============================================" - -cmd /c "shutdown /a 2>nul" *>$null - -if ($failed -gt 0) { exit 1 } -exit 0 diff --git a/playbook/shopfloor-setup/Keyence/09-Setup-Keyence.ps1 b/playbook/shopfloor-setup/Keyence/09-Setup-Keyence.ps1 index e9ac0b8..2b3154c 100644 --- a/playbook/shopfloor-setup/Keyence/09-Setup-Keyence.ps1 +++ b/playbook/shopfloor-setup/Keyence/09-Setup-Keyence.ps1 @@ -1,40 +1,26 @@ # 09-Setup-Keyence.ps1 - Keyence type setup (runs during shopfloor-setup phase). # -# Performs one-shot imaging-time install and then registers the ongoing -# enforcer. Mirrors CMM's pattern. -# -# Sequence: -# 1. Run Install-FromManifest against the staged bundle in $PSScriptRoot. -# Installs VR-6000 Series Software MSI + KEYENCE VR Series USB driver. -# 2. Stage Install-FromManifest.ps1 + Keyence-Enforce.ps1 + keyence-manifest.json -# to C:\Program Files\GE\Keyence so the scheduled task has them post-imaging. -# 3. Register "GE Keyence Enforce" scheduled task (SYSTEM, logon trigger). -# It mounts the tsgwp00525 share, reads the manifest there, and upgrades -# anything whose detection falls out of sync. Credentials for the share -# arrive via Azure DSC writing to HKLM:\SOFTWARE\GE\SFLD\Credentials. +# Performs the imaging-time install of Keyence VR-6000 Series Software MSI + +# KEYENCE VR Series USB driver from the staged bundle. Ongoing enforcement +# is handled by GE-Enforce (registered separately in Run-ShopfloorSetup.ps1) +# reading keyence/manifest.json from the tsgwp00525 share. # # Layout at $PSScriptRoot (xcopied by startnet.cmd only for PCTYPE=Keyence): # keyence-manifest.json # 09-Setup-Keyence.ps1 (this file) -# Keyence-Enforce.ps1 (staged to C:\Program Files\GE\Keyence) -# lib\Install-FromManifest.ps1 (staged alongside) # installers\VR-6000 Series Software.msi # drivers\keyence_vr_series.inf (+ cat + amd64\{Wdf,WinUsb}CoInstaller*.dll) # +# Library lookup: the imaging-time install uses the common Install-FromManifest +# library at ..\common\lib\Install-FromManifest.ps1 (relative to $PSScriptRoot). +# # Log: C:\Logs\Keyence\09-Setup-Keyence.log # C:\Logs\Keyence\install.log (written by Install-FromManifest) $ErrorActionPreference = 'Continue' $manifestPath = Join-Path $PSScriptRoot 'keyence-manifest.json' -$libSource = Join-Path $PSScriptRoot 'lib\Install-FromManifest.ps1' -$enforceSource = Join-Path $PSScriptRoot 'Keyence-Enforce.ps1' - -$runtimeRoot = 'C:\Program Files\GE\Keyence' -$runtimeLibDir = Join-Path $runtimeRoot 'lib' -$runtimeLib = Join-Path $runtimeLibDir 'Install-FromManifest.ps1' -$runtimeEnforce = Join-Path $runtimeRoot 'Keyence-Enforce.ps1' -$runtimeManifest= Join-Path $runtimeRoot 'keyence-manifest.json' +$libSource = Join-Path $PSScriptRoot '..\common\lib\Install-FromManifest.ps1' $logDir = 'C:\Logs\Keyence' $installLog = Join-Path $logDir 'install.log' @@ -83,62 +69,6 @@ if (-not (Test-Path $manifestPath)) { Write-KeyenceLog "Install-FromManifest returned $rc" } -# ============================================================================ -# Step 2: Stage runtime scripts to C:\Program Files\GE\Keyence -# ============================================================================ -# These survive past any bootstrap cleanup so the logon-triggered scheduled -# task can run them. The manifest is staged too as a fallback for the first -# logon if the share is unreachable. -Write-KeyenceLog "Staging runtime scripts to $runtimeRoot" -foreach ($dir in @($runtimeRoot, $runtimeLibDir)) { - if (-not (Test-Path $dir)) { - New-Item -Path $dir -ItemType Directory -Force | Out-Null - } -} -Copy-Item -Path $libSource -Destination $runtimeLib -Force -Copy-Item -Path $enforceSource -Destination $runtimeEnforce -Force -Copy-Item -Path $manifestPath -Destination $runtimeManifest -Force - -# ============================================================================ -# Step 3: Register "GE Keyence Enforce" scheduled task (logon trigger, SYSTEM) -# ============================================================================ -$taskName = 'GE Keyence Enforce' - -# Drop any stale version first so re-imaging is idempotent. -$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue -if ($existing) { - Write-KeyenceLog "Removing existing scheduled task '$taskName'" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} - -Write-KeyenceLog "Registering scheduled task '$taskName' (logon trigger, SYSTEM)" -try { - $action = New-ScheduledTaskAction ` - -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runtimeEnforce`"" - - $trigger = New-ScheduledTaskTrigger -AtLogOn - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Hours 1) ` - -MultipleInstances IgnoreNew - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'GE Keyence: enforce VR-6000 Series Software + USB driver against tsgwp00525 SFLD share on user logon' | Out-Null - - Write-KeyenceLog "Scheduled task registered" -} catch { - Write-KeyenceLog "Failed to register scheduled task: $_" "ERROR" -} - Write-KeyenceLog "================================================================" Write-KeyenceLog "=== Keyence Setup session end ===" Write-KeyenceLog "================================================================" diff --git a/playbook/shopfloor-setup/Keyence/Keyence-Enforce.ps1 b/playbook/shopfloor-setup/Keyence/Keyence-Enforce.ps1 deleted file mode 100644 index 4ee3ab5..0000000 --- a/playbook/shopfloor-setup/Keyence/Keyence-Enforce.ps1 +++ /dev/null @@ -1,126 +0,0 @@ -# Keyence-Enforce.ps1 - On-logon Keyence app enforcer (mini-DSC side). -# -# Runs under a SYSTEM scheduled task triggered at user logon. Mounts the -# tsgwp00525 SFLD share using creds written to HKLM by Azure DSC, reads -# keyence-manifest.json from the share, and hands off to Install-FromManifest.ps1 -# which installs anything whose detection fails. -# -# When Keyence ships a VR-6000 update: drop the new MSI (or driver package) -# in the tsgwp00525 share alongside a bumped keyence-manifest.json, and every -# Keyence PC upgrades on its next logon. -# -# Same graceful-degradation pattern as CMM-Enforce: SFLD creds missing, share -# unreachable, or per-app install failure all log and continue. Task never -# returns non-zero so the "last run" UI stays clean; read the log for truth. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\Keyence' -$libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$logDir = 'C:\Logs\Keyence' -$logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) -$driveLetter = 'S:' - -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-EnforceLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue -} - -Write-EnforceLog "================================================================" -Write-EnforceLog "=== Keyence-Enforce session start (PID $PID, user $env:USERNAME) ===" -Write-EnforceLog "================================================================" - -# --- Load site-config + pcProfile (for keyenceSharePath) --- -# Dot-source the same Get-PCProfile.ps1 used during imaging. It populates -# $pcProfile/$siteConfig script variables from C:\Enrollment\site-config.json. -$getProfileScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Get-PCProfile.ps1' -if (-not (Test-Path $getProfileScript)) { - Write-EnforceLog "Get-PCProfile.ps1 not found at $getProfileScript - is this a Keyence PC?" "ERROR" - exit 0 -} -. $getProfileScript - -if (-not $pcProfile -or -not $pcProfile.keyenceSharePath) { - Write-EnforceLog "No keyenceSharePath in profile - nothing to enforce" "WARN" - exit 0 -} - -$sharePath = $pcProfile.keyenceSharePath -Write-EnforceLog "Share: $sharePath" - -# --- SFLD credential lookup (written by Azure DSC post-PPKG). Bail gracefully -# if the creds are not yet provisioned; next logon will retry. --- -function Get-SFLDCredential { - param([string]$ServerName) - $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' - if (-not (Test-Path $basePath)) { return $null } - - foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if (-not $props -or -not $props.TargetHost) { continue } - if ($props.TargetHost -eq $ServerName -or - $props.TargetHost -like "$ServerName.*" -or - $ServerName -like "$($props.TargetHost).*") { - return @{ - Username = $props.Username - Password = $props.Password - TargetHost = $props.TargetHost - KeyName = $entry.PSChildName - } - } - } - return $null -} - -$serverName = ($sharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1 -$cred = Get-SFLDCredential -ServerName $serverName - -if (-not $cred -or -not $cred.Username -or -not $cred.Password) { - Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry at next logon" - exit 0 -} - -Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" - -# --- Mount the share --- -net use $driveLetter /delete /y 2>$null | Out-Null - -$netResult = & net use $driveLetter $sharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" "WARN" - Write-EnforceLog "Share unreachable - probably off-network. Will retry at next logon." - exit 0 -} -Write-EnforceLog "Mounted $sharePath as $driveLetter" - -try { - $manifestOnShare = Join-Path $driveLetter 'keyence-manifest.json' - if (-not (Test-Path $manifestOnShare)) { - Write-EnforceLog "keyence-manifest.json not found on share - nothing to enforce" "WARN" - return - } - - if (-not (Test-Path $libPath)) { - Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" "ERROR" - return - } - - Write-EnforceLog "Handing off to Install-FromManifest.ps1 (InstallerRoot=$driveLetter)" - & $libPath -ManifestPath $manifestOnShare -InstallerRoot $driveLetter -LogFile $logFile - $rc = $LASTEXITCODE - Write-EnforceLog "Install-FromManifest returned $rc" -} -finally { - net use $driveLetter /delete /y 2>$null | Out-Null - Write-EnforceLog "Unmounted $driveLetter" - Write-EnforceLog "=== Keyence-Enforce session end ===" -} - -# Always return 0 so the scheduled task never shows "last run failed" noise. -exit 0 diff --git a/playbook/shopfloor-setup/Keyence/keyence-manifest.json b/playbook/shopfloor-setup/Keyence/keyence-manifest.json index f75af22..4853ed0 100644 --- a/playbook/shopfloor-setup/Keyence/keyence-manifest.json +++ b/playbook/shopfloor-setup/Keyence/keyence-manifest.json @@ -1,6 +1,6 @@ { "Version": "1.0", - "_comment": "Keyence machine-app manifest. Consumed by both 09-Setup-Keyence.ps1 (at imaging time, reading from the in-repo shopfloor-setup/Keyence/ dir xcopied by startnet.cmd) and Keyence-Enforce.ps1 (on logon, reading from the tsgwp00525 share). Each entry has an InstallerRoot-relative 'Installer' path plus standard detection. The 'Type' field is MSI, EXE, or INF: INF invokes pnputil /add-driver /install. When releasing a new VR-6000 version, update the Installer path + DetectionValue here, drop the new MSI on the tsgwp00525 share, and every Keyence PC picks it up on next logon.", + "_comment": "Keyence machine-app manifest, imaging-time only. Consumed by 09-Setup-Keyence.ps1, reading from the in-repo shopfloor-setup/Keyence/ dir xcopied by startnet.cmd. Ongoing enforcement is handled separately by GE-Enforce reading keyence/manifest.json from the tsgwp00525 share. Each entry has an InstallerRoot-relative 'Installer' path plus standard detection. The 'Type' field is MSI, EXE, or INF: INF invokes pnputil /add-driver /install. When releasing a new VR-6000 version, update the Installer path + DetectionValue here AND on the share manifest.", "Applications": [ { "_comment": "VR-6000 Series Software - main Keyence microscope/profilometer control app. Extracted from Keyence6000.exe (Inno Setup wrapper around an InstallShield 2019 MSI). Silent install works fine with /qn + REBOOT=ReallySuppress as long as you bypass the Inno wrapper - the wrapper's [Run] entry calls the bundled InstallShield Setup.exe without silent flags which hangs in session 0.", diff --git a/playbook/shopfloor-setup/Keyence/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/Keyence/lib/Install-FromManifest.ps1 deleted file mode 100644 index ea3c033..0000000 --- a/playbook/shopfloor-setup/Keyence/lib/Install-FromManifest.ps1 +++ /dev/null @@ -1,327 +0,0 @@ -# Install-FromManifest.ps1 - Generic JSON-manifest installer for Keyence apps. -# -# Forked from CMM/lib/Install-FromManifest.ps1 with two additions to support -# the Keyence USB driver: -# - DetectionMethod "pnputil": matches pnputil /enum-drivers output against -# a regex (manifest field: DetectionPattern). -# - Type "INF": installs a driver package via pnputil /add-driver /install. -# -# Every Keyence manifest entry applies to Keyence PCs (no PCTypes filter; -# Keyence-Enforce.ps1 only runs on Keyence PCs by virtue of where the -# scheduled task is registered). -# -# Called from: -# - 09-Setup-Keyence.ps1 at imaging time with -# InstallerRoot= -# - Keyence-Enforce.ps1 on logon with InstallerRoot= -# -# Returns via exit code: 0 if every required app is either already installed -# or installed successfully; non-zero if any install failed. - -param( - [Parameter(Mandatory=$true)] - [string]$ManifestPath, - - [Parameter(Mandatory=$true)] - [string]$InstallerRoot, - - [Parameter(Mandatory=$true)] - [string]$LogFile -) - -$ErrorActionPreference = 'Continue' - -$logDir = Split-Path -Parent $LogFile -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-InstallLog { - param( - [Parameter(Mandatory=$true, Position=0)] - [string]$Message, - [Parameter(Position=1)] - [ValidateSet('INFO','WARN','ERROR')] - [string]$Level = 'INFO' - ) - $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "[$stamp] [$Level] $Message" - Write-Host $line - - # Synchronous write-through so each line hits disk immediately, mirroring - # the preinstall runner's approach - protects forensic trail if an installer - # triggers a reboot mid-loop. - try { - $fs = New-Object System.IO.FileStream( - $LogFile, - [System.IO.FileMode]::Append, - [System.IO.FileAccess]::Write, - [System.IO.FileShare]::Read, - 4096, - [System.IO.FileOptions]::WriteThrough - ) - $bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n") - $fs.Write($bytes, 0, $bytes.Length) - $fs.Flush() - $fs.Dispose() - } catch { - Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue - } -} - -Write-InstallLog "================================================================" -Write-InstallLog "=== Install-FromManifest session start (PID $PID) ===" -Write-InstallLog "Manifest: $ManifestPath" -Write-InstallLog "InstallerRoot: $InstallerRoot" -Write-InstallLog "================================================================" - -if (-not (Test-Path -LiteralPath $ManifestPath)) { - Write-InstallLog "Manifest not found: $ManifestPath" "ERROR" - exit 2 -} - -if (-not (Test-Path -LiteralPath $InstallerRoot)) { - Write-InstallLog "InstallerRoot not found: $InstallerRoot" "ERROR" - exit 2 -} - -try { - $config = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -} catch { - Write-InstallLog "Failed to parse manifest: $_" "ERROR" - exit 2 -} - -if (-not $config.Applications) { - Write-InstallLog "No Applications in manifest - nothing to do" - exit 0 -} - -Write-InstallLog "Manifest lists $($config.Applications.Count) app(s)" - -# Detection helper - mirrors the preinstall runner's logic. Registry path + -# optional value name + optional exact value. The exact-value compare is how -# version-pinned drift detection works: bumping DetectionValue in the manifest -# makes the current install "fail" detection and reinstall. -function Test-AppInstalled { - param($App) - - if (-not $App.DetectionMethod) { return $false } - - try { - switch ($App.DetectionMethod) { - "Registry" { - if (-not (Test-Path $App.DetectionPath)) { return $false } - if ($App.DetectionName) { - $value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue - if (-not $value) { return $false } - if ($App.DetectionValue) { - return ($value.$($App.DetectionName) -eq $App.DetectionValue) - } - return $true - } - return $true - } - "File" { - return Test-Path $App.DetectionPath - } - "FileVersion" { - # Compare a file's VersionInfo.FileVersion against the - # manifest's expected value. Used for version-pinned MSI/EXE - # installs where existence alone doesn't tell you whether - # the right release is on disk. Exact string match - the - # manifest must carry the exact version the vendor stamps - # into the binary. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " FileVersion detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-Item $App.DetectionPath -ErrorAction Stop).VersionInfo.FileVersion - if (-not $actual) { return $false } - return ($actual -eq $App.DetectionValue) - } - "Hash" { - # Compare SHA256 of the on-disk file against the manifest's - # expected value. Used for content-versioned files that do not - # expose a DisplayVersion (secrets like eMxInfo.txt). Bumping - # DetectionValue in the manifest and replacing the file on the - # share is the entire update workflow. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " Hash detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-FileHash -Path $App.DetectionPath -Algorithm SHA256 -ErrorAction Stop).Hash - return ($actual -ieq $App.DetectionValue) - } - "pnputil" { - # Driver package detection via `pnputil /enum-drivers`. The - # DetectionPattern is a regex matched against the full output; - # a hit on the original INF filename (e.g. 'keyence_vr_series\.inf') - # means the package is staged in the DriverStore. - if (-not $App.DetectionPattern) { - Write-InstallLog " pnputil detection requires DetectionPattern - treating as not installed" "WARN" - return $false - } - try { - $enum = & pnputil.exe /enum-drivers 2>&1 | Out-String - return ($enum -match $App.DetectionPattern) - } catch { - Write-InstallLog " pnputil /enum-drivers failed: $_" "WARN" - return $false - } - } - default { - Write-InstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN" - return $false - } - } - } catch { - Write-InstallLog " Detection check threw: $_" "WARN" - return $false - } -} - -$installed = 0 -$skipped = 0 -$failed = 0 - -foreach ($app in $config.Applications) { - # Cancel any pending reboot scheduled by a previous installer, same as the - # preinstall runner. Some Burn bundles schedule a reboot even with /norestart - # (chained bootstrapper ignores the flag for some internal prereqs). - cmd /c "shutdown /a 2>nul" *>$null - - Write-InstallLog "==> $($app.Name)" - - if (Test-AppInstalled -App $app) { - Write-InstallLog " Already installed at expected version - skipping" - $skipped++ - continue - } - - $installerPath = Join-Path $InstallerRoot $app.Installer - if (-not (Test-Path -LiteralPath $installerPath)) { - Write-InstallLog " Installer file not found: $installerPath" "ERROR" - $failed++ - continue - } - - Write-InstallLog " Installing from $installerPath" - if ($app.InstallArgs) { - Write-InstallLog " InstallArgs: $($app.InstallArgs)" - } - - try { - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden - - if ($app.Type -eq "MSI") { - $safeName = $app.Name -replace '[^a-zA-Z0-9]','_' - $msiLog = Join-Path $logDir "msi-$safeName.log" - if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue } - - $psi.FileName = "msiexec.exe" - $psi.Arguments = "/i `"$installerPath`"" - if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs } - $psi.Arguments += " /L*v `"$msiLog`"" - Write-InstallLog " msiexec verbose log: $msiLog" - } - elseif ($app.Type -eq "EXE") { - $psi.FileName = $installerPath - if ($app.InstallArgs) { $psi.Arguments = $app.InstallArgs } - if ($app.LogFile) { - Write-InstallLog " Installer log: $($app.LogFile)" - } - } - elseif ($app.Type -eq "INF") { - # Driver package: stage to Windows DriverStore via pnputil. The - # /install flag binds the driver to any matching hardware currently - # present; drivers without a bound device still persist in the - # store and attach when hardware is plugged in later. - $psi.FileName = "pnputil.exe" - $psi.Arguments = "/add-driver `"$installerPath`" /install" - } - else { - Write-InstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR" - $failed++ - continue - } - - # Use Process.Start directly rather than Start-Process because PS 5.1's - # Start-Process -PassThru disposes the process handle when control returns, - # making ExitCode read as $null. Direct Process.Start gives us a live handle. - $proc = [System.Diagnostics.Process]::Start($psi) - - # No per-app timeout here - PC-DMIS bundles can run 20+ minutes on slow - # disks and we don't want to kill them mid-chain. The calling script - # controls overall session timing. - $proc.WaitForExit() - $exitCode = $proc.ExitCode - - # Burn and MSI exit codes: - # 0 success - # 1641 success, reboot initiated - # 3010 success, reboot pending - if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010) { - Write-InstallLog " Exit code $exitCode - SUCCESS" - if ($exitCode -eq 3010) { Write-InstallLog " (Reboot pending for $($app.Name))" } - if ($exitCode -eq 1641) { Write-InstallLog " (Installer initiated a reboot for $($app.Name))" } - $installed++ - } - else { - Write-InstallLog " Exit code $exitCode - FAILED" "ERROR" - - if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) { - Write-InstallLog " --- last 30 lines of $($app.LogFile) ---" - Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - Write-InstallLog " --- end installer log tail ---" - } - - if ($app.Type -eq "MSI" -and $msiLog -and (Test-Path $msiLog)) { - Write-InstallLog " --- meaningful lines from $msiLog ---" - $patterns = @( - 'Note: 1: ', - 'return value 3', - 'Error \d+\.', - 'CustomAction .* returned actual error', - 'Failed to ', - 'Installation failed', - '1: 2262', - '1: 2203', - '1: 2330' - ) - $regex = ($patterns -join '|') - $matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue | - Select-Object -First 30 - if ($matches) { - foreach ($m in $matches) { Write-InstallLog " $($m.Line.Trim())" } - } else { - Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - } - Write-InstallLog " --- end MSI log scan ---" - } - - $failed++ - } - } catch { - Write-InstallLog " Install threw: $_" "ERROR" - $failed++ - } -} - -Write-InstallLog "============================================" -Write-InstallLog "Install-FromManifest complete: $installed installed, $skipped skipped, $failed failed" -Write-InstallLog "============================================" - -cmd /c "shutdown /a 2>nul" *>$null - -if ($failed -gt 0) { exit 1 } -exit 0 diff --git a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 index 97cd9b0..5d43883 100644 --- a/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 +++ b/playbook/shopfloor-setup/Run-ShopfloorSetup.ps1 @@ -165,7 +165,7 @@ foreach ($name in $runAfterTypeSpecific) { Write-Host "Shopfloor setup complete for $pcType." # --- Copy utility scripts to SupportUser desktop --- -foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat')) { +foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat', 'SetShopfloorAutoLogon.bat')) { $src = Join-Path $setupDir "Shopfloor\$tool" if (Test-Path $src) { Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force @@ -288,14 +288,9 @@ Unregister-ScheduledTask -TaskName 'GE Re-enable Wired NICs' -Confirm:$false -Er $commonSetupDir = Join-Path $PSScriptRoot 'common' # --- Register the unified GE-Enforce scheduled task --- -# Replaces the per-type legacy enforcers (CMM-Enforce, Keyence-Enforce, -# Machine-Enforce, Common-Enforce, Acrobat-Enforce). Register-GEEnforce.ps1 -# unregisters any of those legacy tasks before creating the new one, so -# running this after the legacy Register-* invocations below is harmless -# and race-free. Once a future repo cleanup retires the legacy Register-* -# scripts entirely, those invocations below can be removed. Until then we -# accept a brief moment of duplicate registration that Register-GEEnforce -# itself resolves. +# Single dispatcher for all PC-type ongoing-update enforcement. Reads +# per-pctype manifest.json from the tsgwp00525 share and processes +# common + per-type + per-type-subtype manifests in order. $registerGE = Join-Path $commonSetupDir 'Register-GEEnforce.ps1' if (Test-Path -LiteralPath $registerGE) { Write-Host "" @@ -316,17 +311,7 @@ if (Test-Path -LiteralPath $registerGE) { Write-Warning "GE-Enforce registration failed: $_" } } else { - Write-Host "Register-GEEnforce.ps1 not found - skipping (legacy per-type enforcers remain active)" -} - -# Legacy Common enforcer: kept for the transition period; GE-Enforce -# unregisters the task it creates. Remove this block when the legacy -# Common-Enforce.ps1 is retired from the repo. -$registerCommon = Join-Path $commonSetupDir 'Register-CommonEnforce.ps1' -if (Test-Path -LiteralPath $registerCommon) { - Write-Host "" - Write-Host "=== (legacy) Registering Common Apps enforcer - will be superseded by GE-Enforce ===" - try { & $registerCommon } catch { Write-Warning "Common enforce registration failed: $_" } + Write-Warning "Register-GEEnforce.ps1 not found - no ongoing enforcement will run on this PC" } # Map S: drive on user logon for every account in BUILTIN\Users. The @@ -342,20 +327,6 @@ if (Test-Path -LiteralPath $registerMapShare) { Write-Host "Register-MapSfldShare.ps1 not found (optional) - skipping" } -# Standard-Machine gets a machine-apps enforcer (UDC, eDNC, NTLARS) that -# replaced the Intune DSC path (DSC has no sub-type awareness and was -# pushing these to Timeclocks). Timeclocks skip this registration. -if ($pcType -eq "Standard" -and $pcSubType -eq "Machine") { - $registerMachine = Join-Path $setupDir "Standard\Register-MachineEnforce.ps1" - if (Test-Path -LiteralPath $registerMachine) { - Write-Host "" - Write-Host "=== Registering Machine-apps enforcer ===" - try { & $registerMachine } catch { Write-Warning "Machine enforce registration failed: $_" } - } else { - Write-Host "Register-MachineEnforce.ps1 not found (optional) - skipping" - } -} - # --- Run enrollment (PPKG install) --- # Enrollment is the LAST thing we do. Install-ProvisioningPackage triggers # an immediate reboot -- everything after this call is unlikely to execute. diff --git a/playbook/shopfloor-setup/Standard/Machine-Enforce.ps1 b/playbook/shopfloor-setup/Standard/Machine-Enforce.ps1 deleted file mode 100644 index f4cb23b..0000000 --- a/playbook/shopfloor-setup/Standard/Machine-Enforce.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -# Machine-Enforce.ps1 - On-logon enforcer for Standard-Machine shopfloor apps -# (UDC, eDNC, NTLARS, future additions). -# -# Runs under a SYSTEM scheduled task triggered at user logon on Standard-Machine -# PCs only (Timeclock PCs skip registration). Mirrors CMM-Enforce / Acrobat- -# Enforce: mounts the SFLD share, reads machineapps-manifest.json from the -# share, hands off to Install-FromManifest.ps1 which installs anything whose -# detection fails. -# -# Why this exists: Intune DSC's main-category YAML used to handle UDC/eDNC/ -# NTLARS enforcement, but DSC has no pc-subtype awareness so Timeclocks in -# category=main got Machine-only apps like UDC pushed to them. These apps -# were pulled from the DSC YAML; this enforcer replaces their drift-correction -# behavior while leaving initial install to the imaging preinstall phase. -# -# Graceful degradation mirrors CMM-Enforce: -# - SFLD creds missing (Azure DSC has not run yet) -> log + exit 0 -# - Share unreachable (network, VPN) -> log + exit 0 -# - Install failure on any one app -> log + continue with next -# -# Never returns non-zero to the task scheduler; failures show up in the log. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\MachineApps' -$libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$logDir = 'C:\Logs\MachineApps' -$logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) -# Use a drive letter that does not clash with CMM-Enforce (S:) or -# Acrobat-Enforce (T:) so enforcers can run concurrently at logon. -$driveLetter = 'U:' - -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-EnforceLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue -} - -Write-EnforceLog "================================================================" -Write-EnforceLog "=== Machine-Enforce session start (PID $PID, user $env:USERNAME) ===" -Write-EnforceLog "================================================================" - -# --- Gate: this enforcer is Standard-Machine only. --- -# Belt-and-suspenders: registration is already Machine-only, but double-check -# so a manual copy to a Timeclock PC would no-op instead of chewing through -# the manifest on a device that shouldn't run it. -$subtypeFile = 'C:\Enrollment\pc-subtype.txt' -if (Test-Path $subtypeFile) { - $sub = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim() - if ($sub -and $sub -ne 'Machine') { - Write-EnforceLog "pc-subtype is '$sub' (not Machine) - exiting" - exit 0 - } -} - -# --- Load site-config for machineappsSharePath --- -$getProfileScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Get-PCProfile.ps1' -if (-not (Test-Path $getProfileScript)) { - Write-EnforceLog "Get-PCProfile.ps1 not found at $getProfileScript - cannot locate share" "ERROR" - exit 0 -} -. $getProfileScript - -if (-not $pcProfile -or -not $pcProfile.machineappsSharePath) { - Write-EnforceLog "No machineappsSharePath in profile - nothing to enforce" "WARN" - exit 0 -} - -$sharePath = $pcProfile.machineappsSharePath -Write-EnforceLog "Share: $sharePath" - -# --- SFLD credential lookup (written by Azure DSC post-PPKG). Bail -# gracefully if the creds haven't been provisioned yet. --- -function Get-SFLDCredential { - param([string]$ServerName) - $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' - if (-not (Test-Path $basePath)) { return $null } - - foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if (-not $props -or -not $props.TargetHost) { continue } - if ($props.TargetHost -eq $ServerName -or - $props.TargetHost -like "$ServerName.*" -or - $ServerName -like "$($props.TargetHost).*") { - return @{ - Username = $props.Username - Password = $props.Password - TargetHost = $props.TargetHost - KeyName = $entry.PSChildName - } - } - } - return $null -} - -$serverName = ($sharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1 -$cred = Get-SFLDCredential -ServerName $serverName - -if (-not $cred -or -not $cred.Username -or -not $cred.Password) { - Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry at next logon" - exit 0 -} - -Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" - -# --- Mount the share --- -net use $driveLetter /delete /y 2>$null | Out-Null - -$netResult = & net use $driveLetter $sharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" "WARN" - Write-EnforceLog "Share unreachable - probably off-network. Will retry at next logon." - exit 0 -} -Write-EnforceLog "Mounted $sharePath as $driveLetter" - -try { - $manifestOnShare = Join-Path $driveLetter 'machineapps-manifest.json' - if (-not (Test-Path $manifestOnShare)) { - Write-EnforceLog "machineapps-manifest.json not found on share - nothing to enforce" "WARN" - return - } - - if (-not (Test-Path $libPath)) { - Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" "ERROR" - return - } - - Write-EnforceLog "Handing off to Install-FromManifest.ps1 (InstallerRoot=$driveLetter)" - & $libPath -ManifestPath $manifestOnShare -InstallerRoot $driveLetter -LogFile $logFile - $rc = $LASTEXITCODE - Write-EnforceLog "Install-FromManifest returned $rc" -} -finally { - net use $driveLetter /delete /y 2>$null | Out-Null - Write-EnforceLog "Unmounted $driveLetter" - Write-EnforceLog "=== Machine-Enforce session end ===" -} - -exit 0 diff --git a/playbook/shopfloor-setup/Standard/Register-MachineEnforce.ps1 b/playbook/shopfloor-setup/Standard/Register-MachineEnforce.ps1 deleted file mode 100644 index 2f9cee5..0000000 --- a/playbook/shopfloor-setup/Standard/Register-MachineEnforce.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -# Register-MachineEnforce.ps1 - One-time setup for the Standard-Machine -# logon-enforce scheduled task. Called by Run-ShopfloorSetup.ps1 on -# Standard-Machine PCs only (Timeclocks skip). Idempotent: re-running -# refreshes staged scripts and re-registers the task. -# -# Parallel to CMM\09-Setup-CMM.ps1 steps 3-4 (stage Install-FromManifest + -# Machine-Enforce, register the task) with no imaging-time install step - -# initial UDC/eDNC/NTLARS install is already handled by the preinstall -# phase on the PXE server. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\MachineApps' -$runtimeLib = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$runtimeEnforce = Join-Path $installRoot 'Machine-Enforce.ps1' -$logDir = 'C:\Logs\MachineApps' -$setupLog = Join-Path $logDir 'setup.log' - -$sourceLib = Join-Path $PSScriptRoot 'lib\Install-FromManifest.ps1' -$sourceEnforce = Join-Path $PSScriptRoot 'Machine-Enforce.ps1' - -if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } -if (-not (Test-Path $installRoot)) { New-Item -Path $installRoot -ItemType Directory -Force | Out-Null } -if (-not (Test-Path (Join-Path $installRoot 'lib'))) { - New-Item -Path (Join-Path $installRoot 'lib') -ItemType Directory -Force | Out-Null -} - -function Write-SetupLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $setupLog -Value $line -ErrorAction SilentlyContinue -} - -Write-SetupLog "=== Register-MachineEnforce start ===" - -foreach ($pair in @( - @{ Src = $sourceLib; Dst = $runtimeLib }, - @{ Src = $sourceEnforce; Dst = $runtimeEnforce } -)) { - if (-not (Test-Path $pair.Src)) { - Write-SetupLog "Source not found: $($pair.Src) - cannot stage" "ERROR" - continue - } - Copy-Item -Path $pair.Src -Destination $pair.Dst -Force - Write-SetupLog "Staged $($pair.Src) -> $($pair.Dst)" -} - -$taskName = 'GE Shopfloor Machine Apps Enforce' -$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue -if ($existing) { - Write-SetupLog "Removing existing scheduled task '$taskName'" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} - -Write-SetupLog "Registering scheduled task '$taskName' (logon trigger, SYSTEM)" -try { - $action = New-ScheduledTaskAction ` - -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runtimeEnforce`"" - - $trigger = New-ScheduledTaskTrigger -AtLogOn - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - # ExecutionTimeLimit 1 hour; UDC/eDNC/NTLARS combined shouldn't exceed that. - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Hours 1) ` - -MultipleInstances IgnoreNew - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'GE Shopfloor Machine: enforce UDC/eDNC/NTLARS version against tsgwp00525 SFLD share on user logon' | Out-Null - - Write-SetupLog "Scheduled task registered" -} catch { - Write-SetupLog "Failed to register scheduled task: $_" "ERROR" -} - -Write-SetupLog "=== Register-MachineEnforce end ===" diff --git a/playbook/shopfloor-setup/Standard/lib/Install-FromManifest.ps1 b/playbook/shopfloor-setup/Standard/lib/Install-FromManifest.ps1 deleted file mode 100644 index 1022cfb..0000000 --- a/playbook/shopfloor-setup/Standard/lib/Install-FromManifest.ps1 +++ /dev/null @@ -1,289 +0,0 @@ -# Install-FromManifest.ps1 - Generic JSON-manifest installer for cross-PC-type -# apps enforced from the SFLD share (Acrobat Reader DC today; others later). -# -# Duplicated from CMM\lib\Install-FromManifest.ps1 with a few differences: -# - adds Type=CMD (cmd.exe /c wrapper, needed for Acrobat's two-step -# MSI + MSP install that the vendor ships as Install-AcroReader.cmd) -# - unchanged otherwise; a future pass will unify both libraries. -# -# Called from: -# - Acrobat-Enforce.ps1 on logon with InstallerRoot= -# -# Returns via exit code: 0 if every required app is either already installed -# or installed successfully; non-zero if any install failed. - -param( - [Parameter(Mandatory=$true)] - [string]$ManifestPath, - - [Parameter(Mandatory=$true)] - [string]$InstallerRoot, - - [Parameter(Mandatory=$true)] - [string]$LogFile -) - -$ErrorActionPreference = 'Continue' - -$logDir = Split-Path -Parent $LogFile -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-InstallLog { - param( - [Parameter(Mandatory=$true, Position=0)] - [string]$Message, - [Parameter(Position=1)] - [ValidateSet('INFO','WARN','ERROR')] - [string]$Level = 'INFO' - ) - $stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "[$stamp] [$Level] $Message" - Write-Host $line - try { - $fs = New-Object System.IO.FileStream( - $LogFile, - [System.IO.FileMode]::Append, - [System.IO.FileAccess]::Write, - [System.IO.FileShare]::Read, - 4096, - [System.IO.FileOptions]::WriteThrough - ) - $bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n") - $fs.Write($bytes, 0, $bytes.Length) - $fs.Flush() - $fs.Dispose() - } catch { - Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue - } -} - -Write-InstallLog "================================================================" -Write-InstallLog "=== Install-FromManifest session start (PID $PID) ===" -Write-InstallLog "Manifest: $ManifestPath" -Write-InstallLog "InstallerRoot: $InstallerRoot" -Write-InstallLog "================================================================" - -if (-not (Test-Path -LiteralPath $ManifestPath)) { - Write-InstallLog "Manifest not found: $ManifestPath" "ERROR" - exit 2 -} - -if (-not (Test-Path -LiteralPath $InstallerRoot)) { - Write-InstallLog "InstallerRoot not found: $InstallerRoot" "ERROR" - exit 2 -} - -try { - $config = Get-Content -LiteralPath $ManifestPath -Raw | ConvertFrom-Json -} catch { - Write-InstallLog "Failed to parse manifest: $_" "ERROR" - exit 2 -} - -if (-not $config.Applications) { - Write-InstallLog "No Applications in manifest - nothing to do" - exit 0 -} - -Write-InstallLog "Manifest lists $($config.Applications.Count) app(s)" - -function Test-AppInstalled { - param($App) - - if (-not $App.DetectionMethod) { return $false } - - try { - switch ($App.DetectionMethod) { - "Registry" { - if (-not (Test-Path $App.DetectionPath)) { return $false } - if ($App.DetectionName) { - $value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue - if (-not $value) { return $false } - if ($App.DetectionValue) { - return ($value.$($App.DetectionName) -eq $App.DetectionValue) - } - return $true - } - return $true - } - "File" { - return Test-Path $App.DetectionPath - } - "FileVersion" { - # Compare a file's VersionInfo.FileVersion against the - # manifest's expected value. Used for version-pinned MSI/EXE - # installs where existence alone doesn't tell you whether - # the right release is on disk (e.g. eDNC 6.4.3 vs 6.4.4 - # both leave NTLARS.exe in the same path). Exact string - # match - the manifest must carry the exact version the - # vendor stamps into the binary. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " FileVersion detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-Item $App.DetectionPath -ErrorAction Stop).VersionInfo.FileVersion - if (-not $actual) { return $false } - return ($actual -eq $App.DetectionValue) - } - "Hash" { - # Compare SHA256 of the on-disk file against the manifest's - # expected value. Used for content-versioned files that do not - # expose a DisplayVersion (secrets like eMxInfo.txt). Bumping - # DetectionValue in the manifest and replacing the file on the - # share is the entire update workflow. - if (-not (Test-Path $App.DetectionPath)) { return $false } - if (-not $App.DetectionValue) { - Write-InstallLog " Hash detection requires DetectionValue - treating as not installed" "WARN" - return $false - } - $actual = (Get-FileHash -Path $App.DetectionPath -Algorithm SHA256 -ErrorAction Stop).Hash - return ($actual -ieq $App.DetectionValue) - } - default { - Write-InstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN" - return $false - } - } - } catch { - Write-InstallLog " Detection check threw: $_" "WARN" - return $false - } -} - -$installed = 0 -$skipped = 0 -$failed = 0 - -foreach ($app in $config.Applications) { - cmd /c "shutdown /a 2>nul" *>$null - - Write-InstallLog "==> $($app.Name)" - - if (Test-AppInstalled -App $app) { - Write-InstallLog " Already installed at expected version - skipping" - $skipped++ - continue - } - - $installerPath = Join-Path $InstallerRoot $app.Installer - if (-not (Test-Path -LiteralPath $installerPath)) { - Write-InstallLog " Installer file not found: $installerPath" "ERROR" - $failed++ - continue - } - - Write-InstallLog " Installing from $installerPath" - if ($app.InstallArgs) { - Write-InstallLog " InstallArgs: $($app.InstallArgs)" - } - - try { - $psi = New-Object System.Diagnostics.ProcessStartInfo - $psi.UseShellExecute = $false - $psi.CreateNoWindow = $true - $psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden - - $msiLog = $null - - if ($app.Type -eq "MSI") { - $safeName = $app.Name -replace '[^a-zA-Z0-9]','_' - $msiLog = Join-Path $logDir "msi-$safeName.log" - if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue } - - $psi.FileName = "msiexec.exe" - $psi.Arguments = "/i `"$installerPath`"" - if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs } - $psi.Arguments += " /L*v `"$msiLog`"" - Write-InstallLog " msiexec verbose log: $msiLog" - } - elseif ($app.Type -eq "EXE") { - $psi.FileName = $installerPath - if ($app.InstallArgs) { $psi.Arguments = $app.InstallArgs } - if ($app.LogFile) { - Write-InstallLog " Installer log: $($app.LogFile)" - } - } - elseif ($app.Type -eq "CMD") { - # .cmd/.bat scripts cannot be executed directly via - # ProcessStartInfo with UseShellExecute=false; route through - # cmd.exe /c. Vendor-provided two-step install wrappers - # (Install-AcroReader.cmd) fit here naturally. - $psi.FileName = "cmd.exe" - $psi.Arguments = "/c `"$installerPath`"" - if ($app.InstallArgs) { $psi.Arguments += " " + $app.InstallArgs } - if ($app.LogFile) { - Write-InstallLog " Installer log: $($app.LogFile)" - } - } - else { - Write-InstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR" - $failed++ - continue - } - - $proc = [System.Diagnostics.Process]::Start($psi) - $proc.WaitForExit() - $exitCode = $proc.ExitCode - - if ($exitCode -eq 0 -or $exitCode -eq 1641 -or $exitCode -eq 3010) { - Write-InstallLog " Exit code $exitCode - SUCCESS" - if ($exitCode -eq 3010) { Write-InstallLog " (Reboot pending for $($app.Name))" } - if ($exitCode -eq 1641) { Write-InstallLog " (Installer initiated a reboot for $($app.Name))" } - $installed++ - } - else { - Write-InstallLog " Exit code $exitCode - FAILED" "ERROR" - - if (($app.Type -eq "EXE" -or $app.Type -eq "CMD") -and $app.LogFile -and (Test-Path $app.LogFile)) { - Write-InstallLog " --- last 30 lines of $($app.LogFile) ---" - Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - Write-InstallLog " --- end installer log tail ---" - } - - if ($app.Type -eq "MSI" -and $msiLog -and (Test-Path $msiLog)) { - Write-InstallLog " --- meaningful lines from $msiLog ---" - $patterns = @( - 'Note: 1: ', - 'return value 3', - 'Error \d+\.', - 'CustomAction .* returned actual error', - 'Failed to ', - 'Installation failed', - '1: 2262', - '1: 2203', - '1: 2330' - ) - $regex = ($patterns -join '|') - $matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue | - Select-Object -First 30 - if ($matches) { - foreach ($m in $matches) { Write-InstallLog " $($m.Line.Trim())" } - } else { - Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object { - Write-InstallLog " $_" - } - } - Write-InstallLog " --- end MSI log scan ---" - } - - $failed++ - } - } catch { - Write-InstallLog " Install threw: $_" "ERROR" - $failed++ - } -} - -Write-InstallLog "============================================" -Write-InstallLog "Install-FromManifest complete: $installed installed, $skipped skipped, $failed failed" -Write-InstallLog "============================================" - -cmd /c "shutdown /a 2>nul" *>$null - -if ($failed -gt 0) { exit 1 } -exit 0 diff --git a/playbook/shopfloor-setup/Standard/machineapps-manifest.template.json b/playbook/shopfloor-setup/Standard/machineapps-manifest.template.json deleted file mode 100644 index 30bf76a..0000000 --- a/playbook/shopfloor-setup/Standard/machineapps-manifest.template.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "Version": "1.0", - "_comment": "Standard-Machine shopfloor app enforcement manifest. This is the TEMPLATE kept in the repo; the authoritative copy lives on the SFLD share at \\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\main\\machineapps\\machineapps-manifest.json. Machine-Enforce.ps1 reads the share copy on every user logon via the 'GE Shopfloor Machine Apps Enforce' scheduled task (registered by Register-MachineEnforce.ps1 at imaging time, Standard-Machine only). Initial install still happens during the preinstall phase on the imaging PXE server; this enforcer is the ongoing drift-correction side. On a freshly-imaged PC detection passes immediately and the enforcer no-ops. Replaces DSC-based enforcement of these apps which was pulled because Intune DSC has no pc-subtype awareness and was pushing UDC/eDNC/NTLARS to Standard-Timeclock PCs.", - "Applications": [ - { - "_comment": "UDC. Install args follow the preinstall.json pattern: Site name in quotes, then machine number placeholder (Configure-PC.ps1 re-runs UDC_Setup with the real machine number after imaging, so the placeholder is overwritten in HKLM at that point). KillAfterDetection is only meaningful during preinstall; the enforcer lets Install-FromManifest wait for the process normally.", - "Name": "UDC", - "Installer": "UDC_Setup.exe", - "Type": "EXE", - "InstallArgs": "\"West Jefferson\" 9999", - "DetectionMethod": "Registry", - "DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC", - "DetectionName": "DisplayVersion", - "DetectionValue": "REPLACE_WITH_PINNED_UDC_VERSION" - }, - { - "_comment": "eDNC 6.4.5. Ships with NTLARS bundled (NTLARS.exe lands at C:\\Program Files (x86)\\Dnc\\Common\\ as part of the same install), so no separate NTLARS entry is needed. SITESELECTED encodes the site (was a recurring bug in early shopfloor-setup scripts that omitted it). Adjust to your site's value if not West Jefferson. Detection uses FileVersion on DncMain.exe so version upgrades actually fire. The vendor stamps DncMain.exe with a 4-part version (e.g. '6.4.5.0'), not 3-part, so DetectionValue must be the exact 4-part string - an earlier 3-part value in this entry caused detection to always fail and the MSI reinstalled silently on every logon. Update workflow: drop the new MSI on the SFLD share, bump DetectionValue + Installer in this manifest to the new vendor-stamped FileVersion, and the next user logon installs it.", - "Name": "eDNC (bundles NTLARS)", - "Installer": "eDNC_6-4-5.msi", - "Type": "MSI", - "InstallArgs": "/qn /norestart ALLUSERS=1 REBOOT=ReallySuppress SITESELECTED=\"West Jefferson\"", - "DetectionMethod": "FileVersion", - "DetectionPath": "C:\\Program Files (x86)\\Dnc\\bin\\DncMain.exe", - "DetectionValue": "6.4.5.0" - }, - { - "_comment": "Custom eMxInfo.txt (site-specific eDNC config). No vendor installer - the secret file lives on the SFLD share alongside the eDNC MSI. Install-eMxInfo.cmd copies it to both 32-bit and 64-bit eDNC Program Files paths. Hash detection catches both 'file missing' and 'file is a stale version'. Yearly rotation procedure: drop the new eMxInfo.txt on the share, recompute its SHA256 (PowerShell: (Get-FileHash .\\eMxInfo.txt -Algorithm SHA256).Hash), paste the new hash into DetectionValue here, save. Every Machine PC catches up on the next user logon. Content-sensitive: eMxInfo.txt must NEVER be committed to git (already in .gitignore).", - "Name": "eMxInfo.txt", - "Installer": "Install-eMxInfo.cmd", - "Type": "CMD", - "DetectionMethod": "Hash", - "DetectionPath": "C:\\Program Files\\eDNC\\eMxInfo.txt", - "DetectionValue": "87733201CB11E7343BD432F1E303FBF41DB58EBAAEFF37BD4C3C9B267B145A20" - } - ] -} diff --git a/playbook/shopfloor-setup/common/Acrobat-Enforce.ps1 b/playbook/shopfloor-setup/common/Acrobat-Enforce.ps1 deleted file mode 100644 index ded76c2..0000000 --- a/playbook/shopfloor-setup/common/Acrobat-Enforce.ps1 +++ /dev/null @@ -1,134 +0,0 @@ -# Acrobat-Enforce.ps1 - On-logon Adobe Acrobat Reader DC enforcer. -# -# Cross-PC-type companion to CMM-Enforce.ps1. Runs under a SYSTEM scheduled -# task triggered at user logon on every PC regardless of PC type, mounts the -# tsgwp00525 SFLD share (common\acrobat path) using SFLD creds provisioned -# by Azure DSC, reads acrobat-manifest.json from the share, and hands off to -# Install-FromManifest.ps1 which installs anything whose detection fails. -# -# Initial Acrobat install happens at imaging time via the preinstall flow -# (playbook/preinstall/...). This enforcer is the ongoing-updates side: when -# Adobe publishes a new quarterly DC patch, drop the new .msp on the share, -# bump DetectionValue in acrobat-manifest.json, and every PC catches up on -# its next logon. On a freshly-imaged PC, detection passes immediately and -# this script no-ops. -# -# Graceful degradation mirrors CMM-Enforce: -# - SFLD creds missing (Azure DSC has not run yet) -> log + exit 0 -# - Share unreachable (network, VPN) -> log + exit 0 -# - Install failure -> log + exit 0 -# -# Never returns non-zero to the task scheduler; failures show up in the log. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\Acrobat' -$libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$logDir = 'C:\Logs\Acrobat' -$logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) -# Use a drive letter that does not clash with CMM-Enforce's S: drive so the -# two enforcers can run concurrently at logon without fighting for the mount. -$driveLetter = 'T:' - -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-EnforceLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue -} - -Write-EnforceLog "================================================================" -Write-EnforceLog "=== Acrobat-Enforce session start (PID $PID, user $env:USERNAME) ===" -Write-EnforceLog "================================================================" - -# --- Load site-config for acrobatSharePath --- -# Dot-source the same Get-PCProfile.ps1 used at imaging time. It walks -# C:\Enrollment\site-config.json into $pcProfile / $siteConfig script vars. -$getProfileScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Get-PCProfile.ps1' -if (-not (Test-Path $getProfileScript)) { - Write-EnforceLog "Get-PCProfile.ps1 not found at $getProfileScript - cannot locate share" "ERROR" - exit 0 -} -. $getProfileScript - -# Acrobat share lives under the site-config "common" section, which applies -# to every PC type (unlike cmmSharePath which is CMM-only). -if (-not $siteConfig -or -not $siteConfig.common -or -not $siteConfig.common.acrobatSharePath) { - Write-EnforceLog "No common.acrobatSharePath in site-config - nothing to enforce" "WARN" - exit 0 -} - -$sharePath = $siteConfig.common.acrobatSharePath -Write-EnforceLog "Share: $sharePath" - -function Get-SFLDCredential { - param([string]$ServerName) - $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' - if (-not (Test-Path $basePath)) { return $null } - - foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if (-not $props -or -not $props.TargetHost) { continue } - if ($props.TargetHost -eq $ServerName -or - $props.TargetHost -like "$ServerName.*" -or - $ServerName -like "$($props.TargetHost).*") { - return @{ - Username = $props.Username - Password = $props.Password - TargetHost = $props.TargetHost - KeyName = $entry.PSChildName - } - } - } - return $null -} - -$serverName = ($sharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1 -$cred = Get-SFLDCredential -ServerName $serverName - -if (-not $cred -or -not $cred.Username -or -not $cred.Password) { - Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry at next logon" - exit 0 -} - -Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" - -# --- Mount the share --- -net use $driveLetter /delete /y 2>$null | Out-Null - -$netResult = & net use $driveLetter $sharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" "WARN" - Write-EnforceLog "Share unreachable - probably off-network. Will retry at next logon." - exit 0 -} -Write-EnforceLog "Mounted $sharePath as $driveLetter" - -try { - $manifestOnShare = Join-Path $driveLetter 'acrobat-manifest.json' - if (-not (Test-Path $manifestOnShare)) { - Write-EnforceLog "acrobat-manifest.json not found on share - nothing to enforce" "WARN" - return - } - - if (-not (Test-Path $libPath)) { - Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" "ERROR" - return - } - - Write-EnforceLog "Handing off to Install-FromManifest.ps1 (InstallerRoot=$driveLetter)" - & $libPath -ManifestPath $manifestOnShare -InstallerRoot $driveLetter -LogFile $logFile - $rc = $LASTEXITCODE - Write-EnforceLog "Install-FromManifest returned $rc" -} -finally { - net use $driveLetter /delete /y 2>$null | Out-Null - Write-EnforceLog "Unmounted $driveLetter" - Write-EnforceLog "=== Acrobat-Enforce session end ===" -} - -exit 0 diff --git a/playbook/shopfloor-setup/common/Common-Enforce.ps1 b/playbook/shopfloor-setup/common/Common-Enforce.ps1 deleted file mode 100644 index 89e66d1..0000000 --- a/playbook/shopfloor-setup/common/Common-Enforce.ps1 +++ /dev/null @@ -1,123 +0,0 @@ -# Common-Enforce.ps1 - On-logon enforcer for cross-PC-type apps (Acrobat -# Reader, WJF Defect Tracker, future common apps). -# -# Runs under a SYSTEM scheduled task triggered at user logon on every PC -# regardless of PC type. Mounts the tsgwp00525 SFLD share (common\apps -# path) using SFLD creds provisioned by Azure DSC, reads -# common-apps-manifest.json from the share, and hands off to -# Install-FromManifest.ps1 which installs anything whose detection fails. -# -# Update workflow: drop new installer on the share, bump DetectionValue in -# common-apps-manifest.json, every PC catches up on next logon. -# -# Graceful degradation: -# - SFLD creds missing (Azure DSC has not run yet) -> log + exit 0 -# - Share unreachable (network, VPN) -> log + exit 0 -# - Install failure -> log + exit 0 -# -# Never returns non-zero to the task scheduler; failures show up in the log. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\CommonApps' -$libPath = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$logDir = 'C:\Logs\CommonApps' -$logFile = Join-Path $logDir ('enforce-{0}.log' -f (Get-Date -Format 'yyyyMMdd')) -$driveLetter = 'T:' - -if (-not (Test-Path $logDir)) { - New-Item -Path $logDir -ItemType Directory -Force | Out-Null -} - -function Write-EnforceLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue -} - -Write-EnforceLog "================================================================" -Write-EnforceLog "=== Common-Enforce session start (PID $PID, user $env:USERNAME) ===" -Write-EnforceLog "================================================================" - -$getProfileScript = 'C:\Enrollment\shopfloor-setup\Shopfloor\lib\Get-PCProfile.ps1' -if (-not (Test-Path $getProfileScript)) { - Write-EnforceLog "Get-PCProfile.ps1 not found at $getProfileScript - cannot locate share" "ERROR" - exit 0 -} -. $getProfileScript - -if (-not $siteConfig -or -not $siteConfig.common -or -not $siteConfig.common.commonAppsSharePath) { - Write-EnforceLog "No common.commonAppsSharePath in site-config - nothing to enforce" "WARN" - exit 0 -} - -$sharePath = $siteConfig.common.commonAppsSharePath -Write-EnforceLog "Share: $sharePath" - -function Get-SFLDCredential { - param([string]$ServerName) - $basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials' - if (-not (Test-Path $basePath)) { return $null } - - foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) { - $props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue - if (-not $props -or -not $props.TargetHost) { continue } - if ($props.TargetHost -eq $ServerName -or - $props.TargetHost -like "$ServerName.*" -or - $ServerName -like "$($props.TargetHost).*") { - return @{ - Username = $props.Username - Password = $props.Password - TargetHost = $props.TargetHost - KeyName = $entry.PSChildName - } - } - } - return $null -} - -$serverName = ($sharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1 -$cred = Get-SFLDCredential -ServerName $serverName - -if (-not $cred -or -not $cred.Username -or -not $cred.Password) { - Write-EnforceLog "No SFLD credential for $serverName yet (Azure DSC has not provisioned it) - will retry at next logon" - exit 0 -} - -Write-EnforceLog "Credential: $($cred.KeyName) (user: $($cred.Username))" - -net use $driveLetter /delete /y 2>$null | Out-Null - -$netResult = & net use $driveLetter $sharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-EnforceLog "net use failed (exit $LASTEXITCODE): $netResult" "WARN" - Write-EnforceLog "Share unreachable - probably off-network. Will retry at next logon." - exit 0 -} -Write-EnforceLog "Mounted $sharePath as $driveLetter" - -try { - $manifestOnShare = Join-Path $driveLetter 'common-apps-manifest.json' - if (-not (Test-Path $manifestOnShare)) { - Write-EnforceLog "common-apps-manifest.json not found on share - nothing to enforce" "WARN" - return - } - - if (-not (Test-Path $libPath)) { - Write-EnforceLog "Install-FromManifest.ps1 not found at $libPath" "ERROR" - return - } - - Write-EnforceLog "Handing off to Install-FromManifest.ps1 (InstallerRoot=$driveLetter)" - & $libPath -ManifestPath $manifestOnShare -InstallerRoot $driveLetter -LogFile $logFile - $rc = $LASTEXITCODE - Write-EnforceLog "Install-FromManifest returned $rc" -} -finally { - net use $driveLetter /delete /y 2>$null | Out-Null - Write-EnforceLog "Unmounted $driveLetter" - Write-EnforceLog "=== Common-Enforce session end ===" -} - -exit 0 diff --git a/playbook/shopfloor-setup/common/Register-AcrobatEnforce.ps1 b/playbook/shopfloor-setup/common/Register-AcrobatEnforce.ps1 deleted file mode 100644 index fa578de..0000000 --- a/playbook/shopfloor-setup/common/Register-AcrobatEnforce.ps1 +++ /dev/null @@ -1,93 +0,0 @@ -# Register-AcrobatEnforce.ps1 - One-time setup for the Acrobat Reader -# logon-enforce scheduled task. Called by each PC type's shopfloor setup -# (Run-ShopfloorSetup.ps1) after the baseline imaging steps, once per -# fresh install. Idempotent: re-running just refreshes the staged scripts -# and re-registers the task. -# -# Parallel to CMM\09-Setup-CMM.ps1 steps 3-4 (stage Install-FromManifest + -# Acrobat-Enforce, register the "GE Acrobat Enforce" task) but without any -# imaging-time install step - initial Acrobat install is already handled by -# the preinstall flow. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\Acrobat' -$runtimeLib = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$runtimeEnforce = Join-Path $installRoot 'Acrobat-Enforce.ps1' -$logDir = 'C:\Logs\Acrobat' -$setupLog = Join-Path $logDir 'setup.log' - -# Source on the imaged client (staged there by WinPE startnet.cmd via -# shopfloor-setup\common\). -$sourceLib = Join-Path $PSScriptRoot 'lib\Install-FromManifest.ps1' -$sourceEnforce = Join-Path $PSScriptRoot 'Acrobat-Enforce.ps1' - -if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } -if (-not (Test-Path $installRoot)) { New-Item -Path $installRoot -ItemType Directory -Force | Out-Null } -if (-not (Test-Path (Join-Path $installRoot 'lib'))) { - New-Item -Path (Join-Path $installRoot 'lib') -ItemType Directory -Force | Out-Null -} - -function Write-SetupLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $setupLog -Value $line -ErrorAction SilentlyContinue -} - -Write-SetupLog "=== Register-AcrobatEnforce start ===" - -# Stage scripts to their runtime location under Program Files so the -# scheduled task can run them as SYSTEM. -foreach ($pair in @( - @{ Src = $sourceLib; Dst = $runtimeLib }, - @{ Src = $sourceEnforce; Dst = $runtimeEnforce } -)) { - if (-not (Test-Path $pair.Src)) { - Write-SetupLog "Source not found: $($pair.Src) - cannot stage" "ERROR" - continue - } - Copy-Item -Path $pair.Src -Destination $pair.Dst -Force - Write-SetupLog "Staged $($pair.Src) -> $($pair.Dst)" -} - -# Register scheduled task. Unregister any stale copy first so re-imaging is -# idempotent. -$taskName = 'GE Acrobat Enforce' -$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue -if ($existing) { - Write-SetupLog "Removing existing scheduled task '$taskName'" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} - -Write-SetupLog "Registering scheduled task '$taskName' (logon trigger, SYSTEM)" -try { - $action = New-ScheduledTaskAction ` - -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runtimeEnforce`"" - - $trigger = New-ScheduledTaskTrigger -AtLogOn - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - # ExecutionTimeLimit 30 min - Acrobat DC patches are smaller than PC-DMIS - # bundles; 30 min is plenty and keeps a stuck install from lingering. - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` - -MultipleInstances IgnoreNew - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'GE Acrobat: enforce Adobe Acrobat Reader DC version against tsgwp00525 SFLD share on user logon' | Out-Null - - Write-SetupLog "Scheduled task registered" -} catch { - Write-SetupLog "Failed to register scheduled task: $_" "ERROR" -} - -Write-SetupLog "=== Register-AcrobatEnforce end ===" diff --git a/playbook/shopfloor-setup/common/Register-CommonEnforce.ps1 b/playbook/shopfloor-setup/common/Register-CommonEnforce.ps1 deleted file mode 100644 index 92d63db..0000000 --- a/playbook/shopfloor-setup/common/Register-CommonEnforce.ps1 +++ /dev/null @@ -1,91 +0,0 @@ -# Register-CommonEnforce.ps1 - Stage Common-Enforce.ps1 + Install-FromManifest -# and register the 'GE Common Apps Enforce' logon task. Cross-PC-type: called -# from Run-ShopfloorSetup.ps1 for every shopfloor image. -# -# Replaces the former Acrobat-only enforcer with a single task that handles -# all common apps (Acrobat, Defect Tracker, future additions) from one -# manifest on the SFLD share. - -$ErrorActionPreference = 'Continue' - -$installRoot = 'C:\Program Files\GE\CommonApps' -$runtimeLib = Join-Path $installRoot 'lib\Install-FromManifest.ps1' -$runtimeEnforce = Join-Path $installRoot 'Common-Enforce.ps1' -$logDir = 'C:\Logs\CommonApps' -$setupLog = Join-Path $logDir 'setup.log' - -$sourceLib = Join-Path $PSScriptRoot 'lib\Install-FromManifest.ps1' -$sourceEnforce = Join-Path $PSScriptRoot 'Common-Enforce.ps1' - -if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } -if (-not (Test-Path $installRoot)) { New-Item -Path $installRoot -ItemType Directory -Force | Out-Null } -if (-not (Test-Path (Join-Path $installRoot 'lib'))) { - New-Item -Path (Join-Path $installRoot 'lib') -ItemType Directory -Force | Out-Null -} - -function Write-SetupLog { - param([string]$Message, [string]$Level = 'INFO') - $line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message - Write-Host $line - Add-Content -Path $setupLog -Value $line -ErrorAction SilentlyContinue -} - -Write-SetupLog "=== Register-CommonEnforce start ===" - -foreach ($pair in @( - @{ Src = $sourceLib; Dst = $runtimeLib }, - @{ Src = $sourceEnforce; Dst = $runtimeEnforce } -)) { - if (-not (Test-Path $pair.Src)) { - Write-SetupLog "Source not found: $($pair.Src) - cannot stage" "ERROR" - continue - } - Copy-Item -Path $pair.Src -Destination $pair.Dst -Force - Write-SetupLog "Staged $($pair.Src) -> $($pair.Dst)" -} - -# Clean up old Acrobat-only enforcer if present (from prior images). -foreach ($oldTask in @('GE Acrobat Enforce')) { - $old = Get-ScheduledTask -TaskName $oldTask -ErrorAction SilentlyContinue - if ($old) { - Write-SetupLog "Removing legacy task '$oldTask'" - Unregister-ScheduledTask -TaskName $oldTask -Confirm:$false -ErrorAction SilentlyContinue - } -} - -$taskName = 'GE Common Apps Enforce' -$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue -if ($existing) { - Write-SetupLog "Removing existing scheduled task '$taskName'" - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} - -Write-SetupLog "Registering scheduled task '$taskName' (logon trigger, SYSTEM)" -try { - $action = New-ScheduledTaskAction ` - -Execute 'powershell.exe' ` - -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$runtimeEnforce`"" - - $trigger = New-ScheduledTaskTrigger -AtLogOn - $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest - $settings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -ExecutionTimeLimit (New-TimeSpan -Minutes 30) ` - -MultipleInstances IgnoreNew - - Register-ScheduledTask ` - -TaskName $taskName ` - -Action $action ` - -Trigger $trigger ` - -Principal $principal ` - -Settings $settings ` - -Description 'GE Common Apps: enforce Acrobat, Defect Tracker, and other cross-type apps against tsgwp00525 SFLD share on user logon' | Out-Null - - Write-SetupLog "Scheduled task registered" -} catch { - Write-SetupLog "Failed to register scheduled task: $_" "ERROR" -} - -Write-SetupLog "=== Register-CommonEnforce end ===" diff --git a/playbook/shopfloor-setup/common/common-apps-manifest.template.json b/playbook/shopfloor-setup/common/common-apps-manifest.template.json deleted file mode 100644 index 8fddda1..0000000 --- a/playbook/shopfloor-setup/common/common-apps-manifest.template.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "Version": "1.0", - "_comment": "Common cross-PC-type app enforcement manifest. TEMPLATE in repo; authoritative copy on SFLD share at \\\\tsgwp00525.wjs.geaerospace.net\\shared\\dt\\shopfloor\\common\\acrobat\\acrobat-manifest.json. Acrobat-Enforce.ps1 reads the share copy on every user logon. Update workflow: drop new installer on share, bump DetectionValue, next logon catches it.", - "Applications": [ - { - "_comment": "Two-step install (MSI + MST transform, then MSP patch) done via the vendor-shipped Install-AcroReader.cmd wrapper.", - "Name": "Adobe Acrobat Reader DC", - "Installer": "Install-AcroReader.cmd", - "Type": "CMD", - "LogFile": "C:\\Logs\\Acrobat\\install.log", - "DetectionMethod": "Registry", - "DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{AC76BA86-7AD7-1033-7B44-AC0F074E4100}", - "DetectionName": "DisplayVersion", - "DetectionValue": "25.001.20531" - }, - { - "_comment": "WJF Defect Tracker. Replaces the old ClickOnce deployment. MSI installs to C:\\Program Files (x86)\\WJF_Defect_Tracker\\. Update workflow: drop new MSI on share, bump DetectionValue to new ProductVersion, next logon upgrades.", - "Name": "WJF Defect Tracker", - "Installer": "WJF_Defect_Tracker.msi", - "Type": "MSI", - "InstallArgs": "/qn /norestart ALLUSERS=1 REBOOT=ReallySuppress TARGETDIR=\"C:\\Program Files (x86)\\WJF_Defect_Tracker\"", - "DetectionMethod": "FileVersion", - "DetectionPath": "C:\\Program Files (x86)\\WJF_Defect_Tracker\\Defect_Tracker.exe", - "DetectionValue": "1.0.0.102" - } - ] -}