- CMM imaging pipeline: WinPE-staged bootstrap + on-logon enforcer against tsgwp00525 share, manifest-driven installer runner shared via Install-FromManifest.ps1. Installs PC-DMIS 2016/2019 R2, CLM 1.8, goCMM; enables .NET 3.5 prereq; registers GE CMM Enforce logon task for ongoing version enforcement. - Shopfloor serial drivers: StarTech PCIe serial + Prolific PL2303 USB-to-serial via Install-Drivers.cmd wrapper calling pnputil /add-driver /subdirs /install. Scoped to Standard PCs. - OpenText extended to CMM/Keyence/Genspect/WaxAndTrace via preinstall.json PCTypes; Defect Tracker added to CMM profile desktopApps + taskbarPins. - Configure-PC startup-item toggle now persists across the logon sweep via C:\\ProgramData\\GE\\Shopfloor\\startup-overrides.json; 06-OrganizeDesktop Phase 3 respects suppressed items. - Get-ProfileValue helper added to Shopfloor/lib/Get-PCProfile.ps1; distinguishes explicit empty array from missing key (fixes Lab getting Plant Apps in startup because empty array was falsy). - 06-OrganizeDesktop gains transcript logging at C:\\Logs\\SFLD\\ 06-OrganizeDesktop.log and now deletes the stale Shopfloor Intune Sync task when C:\\Enrollment\\sync-complete.txt is present (task was registered with Limited principal and couldn't self-unregister). - startnet.cmd CMM xcopy block (gated on pc-type=CMM) stages the bundle to W:\\CMM-Install during WinPE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
631 lines
26 KiB
PowerShell
631 lines
26 KiB
PowerShell
# 06-OrganizeDesktop.ps1 - Single source of truth for the Public Desktop
|
|
# layout. Builds three category folders and fills them, so end users see
|
|
# a clean desktop instead of 20+ loose icons.
|
|
#
|
|
# Creates three subfolders under C:\Users\Public\Desktop:
|
|
# Office\ - Excel, Word, PowerPoint, Outlook, OneNote, etc.
|
|
# Shopfloor Tools\ - UDC, eDNC, HostExplorer, MarkZebra, PC-DMIS, Hexagon,
|
|
# WJ Shopfloor, Defect_Tracker, NTLARS, etc.
|
|
# Web Links\ - .url shortcuts (internal portals)
|
|
#
|
|
# Two phases:
|
|
# PHASE 1 (Invoke-DesktopSweep): walks everything at the desktop ROOT,
|
|
# classifies each .lnk / .url by filename / extension / .lnk target, and
|
|
# moves it into the right category folder. Unknown items are LEFT AT
|
|
# THE ROOT on purpose. eDNC.lnk and NTLARS.lnk are allowlisted to stay
|
|
# at the root - end users click them too often to bury in a folder.
|
|
#
|
|
# PHASE 2 (Add-ShopfloorToolsApps): materializes specific app shortcuts
|
|
# into Shopfloor Tools\ from known .exe paths (UDC, eDNC, NTLARS). For
|
|
# MSI-advertised shortcuts that don't have a resolvable target (WJ
|
|
# Shopfloor, Defect_Tracker), finds the existing .lnk wherever it lives
|
|
# and copies it in. This runs AFTER the sweep so it overwrites any
|
|
# stale copies the sweeper may have moved in.
|
|
#
|
|
# Also registers an "Organize Public Desktop" scheduled task that re-runs
|
|
# the sweep (phase 1) at every logon. Phase 2 doesn't re-run - the
|
|
# shortcuts it creates are stable and 06 on first run populates them.
|
|
#
|
|
# 07-TaskbarLayout.ps1 reads the result of this script and writes
|
|
# LayoutModification.xml to pin specific shortcuts to the taskbar.
|
|
|
|
$ErrorActionPreference = 'Continue'
|
|
|
|
# ============================================================================
|
|
# Admin check - writing to C:\Users\Public\Desktop\* and registering
|
|
# system-wide scheduled tasks both require elevation. Fail fast if not
|
|
# admin so we don't spam half-successful garbage across the output.
|
|
# ============================================================================
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if (-not $isAdmin) {
|
|
Write-Host ""
|
|
Write-Host "ERROR: 06-OrganizeDesktop.ps1 must run as Administrator." -ForegroundColor Red
|
|
Write-Host " Re-run from an elevated PowerShell:" -ForegroundColor Red
|
|
Write-Host " Start -> type 'powershell' -> right-click -> Run as administrator" -ForegroundColor Red
|
|
Write-Host ""
|
|
exit 1
|
|
}
|
|
|
|
# Transcript. 06 runs as a SYSTEM scheduled task at every logon via the
|
|
# 'Organize Public Desktop' task, so without a transcript its decisions
|
|
# (what got added, what got suppressed) are invisible. Append mode keeps
|
|
# a history across logons; session header + PID makes individual runs
|
|
# easy to find.
|
|
$transcriptDir = 'C:\Logs\SFLD'
|
|
if (-not (Test-Path $transcriptDir)) {
|
|
try { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } catch {}
|
|
}
|
|
$transcriptPath = Join-Path $transcriptDir '06-OrganizeDesktop.log'
|
|
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
|
Write-Host ""
|
|
Write-Host "================================================================"
|
|
Write-Host "=== 06-OrganizeDesktop session start (PID $PID, $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
|
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
|
Write-Host "================================================================"
|
|
|
|
# ============================================================================
|
|
# Cleanup: remove 'Shopfloor Intune Sync' task after sync completes
|
|
#
|
|
# Monitor-IntuneProgress.ps1 registers that task as BUILTIN\Users + RunLevel
|
|
# Limited (needs a user context to show the QR code / Configure-PC prompt),
|
|
# and a Limited task cannot Unregister-ScheduledTask itself. So when sync
|
|
# completes it just writes C:\Enrollment\sync-complete.txt as a marker and
|
|
# the task stays around firing on every logon as a no-op.
|
|
#
|
|
# 06 runs as SYSTEM via its own sweep task, which DOES have permission to
|
|
# delete any task. When we see the marker, drop the stale task so Task
|
|
# Scheduler doesn't keep the dead entry forever.
|
|
# ============================================================================
|
|
$syncCompleteMarker = 'C:\Enrollment\sync-complete.txt'
|
|
$syncTaskName = 'Shopfloor Intune Sync'
|
|
if (Test-Path -LiteralPath $syncCompleteMarker) {
|
|
$syncTask = Get-ScheduledTask -TaskName $syncTaskName -ErrorAction SilentlyContinue
|
|
if ($syncTask) {
|
|
try {
|
|
Unregister-ScheduledTask -TaskName $syncTaskName -Confirm:$false -ErrorAction Stop
|
|
Write-Host "Removed stale '$syncTaskName' task (sync complete, marker present)."
|
|
} catch {
|
|
Write-Warning "Failed to remove '$syncTaskName' task: $_"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Load site config + PC profile
|
|
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
|
|
|
$publicDesktop = 'C:\Users\Public\Desktop'
|
|
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
|
$scriptPath = $MyInvocation.MyCommand.Path
|
|
$scriptDir = Split-Path -Parent $scriptPath
|
|
|
|
# Filenames that always stay at the desktop root regardless of classification.
|
|
# Empty right now - every shortcut lives inside a category folder for a
|
|
# clean end-user desktop. Add entries here if a future shortcut needs to
|
|
# stay pinned at root.
|
|
$keepAtRoot = @()
|
|
|
|
# ============================================================================
|
|
# Phase 1: Sweep loose shortcuts at desktop root into category folders
|
|
# ============================================================================
|
|
function Invoke-DesktopSweep {
|
|
param([string]$DesktopPath)
|
|
|
|
if (-not (Test-Path -LiteralPath $DesktopPath)) {
|
|
Write-Host "Public desktop not found at $DesktopPath - skipping sweep."
|
|
return
|
|
}
|
|
|
|
# Category definitions. Order matters: first match wins. Each category
|
|
# can classify by file Extension, file Name (regex), or .lnk Target
|
|
# (regex, resolved via WScript.Shell).
|
|
$categories = [ordered]@{
|
|
'Office' = @{
|
|
Name = @(
|
|
'^(Excel|Word|PowerPoint|Outlook|OneNote|Access|Publisher|OneDrive)(\s|$)'
|
|
)
|
|
Target = @(
|
|
'\\(EXCEL|WINWORD|POWERPNT|OUTLOOK|ONENOTE|MSACCESS|MSPUB)\.EXE$',
|
|
'\\OneDrive\.exe$'
|
|
)
|
|
}
|
|
'Shopfloor Tools' = @{
|
|
Name = @(
|
|
'^UDC',
|
|
'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
|
|
'Host\s*Explorer', 'ShopFloor', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
|
'OpenText',
|
|
'Defect[_\s-]?Tracker',
|
|
'MarkZebra', 'Zebra',
|
|
'PC-?DMIS',
|
|
'Hexagon', 'CLM\s*Tools',
|
|
'Blancco',
|
|
'Keyence'
|
|
)
|
|
Target = @(
|
|
'\\UDC\.exe$',
|
|
'DncMain\.exe$', 'NTLARS\.exe$', 'DNCRemote',
|
|
'HostExplorer\.exe$', 'hostex32\.exe$', 'humicon15\.exe$',
|
|
'PC-?DMIS',
|
|
'Hexagon',
|
|
'Defect_Tracker\.application$'
|
|
)
|
|
}
|
|
'Web Links' = @{
|
|
Extension = @('.url')
|
|
}
|
|
}
|
|
|
|
# Category folders are created LAZILY - only when we're about to move
|
|
# something into them - so a Display / Wax-Trace PC without Office
|
|
# doesn't get an empty Office\ folder littering the desktop. See
|
|
# Remove-EmptyCategoryFolders at the end of the script for the
|
|
# post-sweep cleanup pass that finishes the job.
|
|
|
|
# WScript.Shell for resolving .lnk targets
|
|
$shell = $null
|
|
try { $shell = New-Object -ComObject WScript.Shell } catch {}
|
|
|
|
# Only sweep files at the desktop root, never recurse into the category
|
|
# folders themselves (that would loop things back).
|
|
$items = @(Get-ChildItem -LiteralPath $DesktopPath -File -ErrorAction SilentlyContinue)
|
|
|
|
$moved = 0
|
|
$skipped = 0
|
|
|
|
foreach ($item in $items) {
|
|
# Allowlist check: never sweep items the user wants to keep visible
|
|
if ($keepAtRoot -contains $item.Name) {
|
|
Write-Host " keep: $($item.Name) (allowlisted)"
|
|
continue
|
|
}
|
|
|
|
$category = $null
|
|
|
|
# 1. Extension match (cheapest)
|
|
foreach ($cat in $categories.Keys) {
|
|
$exts = $categories[$cat].Extension
|
|
if ($exts -and ($exts -contains $item.Extension)) {
|
|
$category = $cat
|
|
break
|
|
}
|
|
}
|
|
|
|
# 2. File name regex match
|
|
if (-not $category) {
|
|
foreach ($cat in $categories.Keys) {
|
|
$pats = $categories[$cat].Name
|
|
if (-not $pats) { continue }
|
|
foreach ($p in $pats) {
|
|
if ($item.BaseName -match $p) {
|
|
$category = $cat
|
|
break
|
|
}
|
|
}
|
|
if ($category) { break }
|
|
}
|
|
}
|
|
|
|
# 3. .lnk target resolution
|
|
if (-not $category -and $item.Extension -ieq '.lnk' -and $shell) {
|
|
try {
|
|
$sc = $shell.CreateShortcut($item.FullName)
|
|
$target = $sc.TargetPath
|
|
if ($target) {
|
|
foreach ($cat in $categories.Keys) {
|
|
$pats = $categories[$cat].Target
|
|
if (-not $pats) { continue }
|
|
foreach ($p in $pats) {
|
|
if ($target -match $p) {
|
|
$category = $cat
|
|
break
|
|
}
|
|
}
|
|
if ($category) { break }
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
if ($category) {
|
|
$destDir = Join-Path $DesktopPath $category
|
|
if (-not (Test-Path -LiteralPath $destDir)) {
|
|
try {
|
|
New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null
|
|
} catch {
|
|
Write-Warning "Failed to create category folder '$category' : $_"
|
|
continue
|
|
}
|
|
}
|
|
$destPath = Join-Path $destDir $item.Name
|
|
try {
|
|
Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop
|
|
Write-Host " moved: $($item.Name) -> $category\"
|
|
$moved++
|
|
} catch {
|
|
Write-Warning "Failed to move $($item.Name) : $_"
|
|
}
|
|
} else {
|
|
$skipped++
|
|
}
|
|
}
|
|
|
|
if ($shell) {
|
|
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null } catch {}
|
|
}
|
|
|
|
Write-Host "Sweep complete: $moved moved, $skipped unclassified (left at root)."
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 2: Materialize specific app shortcuts into Shopfloor Tools\
|
|
#
|
|
# Runs AFTER the sweep. Creates fresh .lnk files for apps we can target by
|
|
# .exe path (UDC, eDNC, NTLARS) and copies existing .lnk files for apps
|
|
# that are MSI-advertised and have no resolvable target (WJ Shopfloor,
|
|
# Defect_Tracker). Overwrites whatever the sweeper may have moved in -
|
|
# this is intentional because fresh shortcuts are more reliable than
|
|
# whatever install crud might be floating around.
|
|
#
|
|
# Each app is CONDITIONAL - if its source isn't present on this PC, the
|
|
# entry is silently skipped. Safe to run on all shopfloor PC types.
|
|
# ============================================================================
|
|
|
|
function New-ShopfloorLnk {
|
|
param(
|
|
[string]$Path,
|
|
[string]$Target,
|
|
[string]$Arguments = '',
|
|
[string]$WorkingDirectory = ''
|
|
)
|
|
$wsh = New-Object -ComObject WScript.Shell
|
|
try {
|
|
$sc = $wsh.CreateShortcut($Path)
|
|
$sc.TargetPath = $Target
|
|
if ($Arguments) { $sc.Arguments = $Arguments }
|
|
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
|
|
elseif ($Target) {
|
|
$parent = Split-Path -Parent $Target
|
|
if ($parent) { $sc.WorkingDirectory = $parent }
|
|
}
|
|
$sc.IconLocation = "$Target,0"
|
|
$sc.Save()
|
|
return $true
|
|
} catch {
|
|
Write-Warning "Failed to create $Path : $_"
|
|
return $false
|
|
} finally {
|
|
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
|
|
}
|
|
}
|
|
|
|
# Look up an existing .lnk file across the paths where installers / ppkgs
|
|
# typically drop them. Used for apps where we don't build the .lnk from
|
|
# an .exe (because the target is an MSI-advertised Darwin descriptor).
|
|
function Find-ExistingLnk {
|
|
param([string]$Filename)
|
|
|
|
$candidates = @(
|
|
(Join-Path $publicDesktop $Filename),
|
|
(Join-Path $shopfloorToolsDir $Filename),
|
|
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$Filename",
|
|
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Shopfloor Tools\$Filename"
|
|
)
|
|
foreach ($c in $candidates) {
|
|
if (Test-Path -LiteralPath $c) { return $c }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Add-ShopfloorToolsApps {
|
|
# App registry - each entry describes how to materialize one shortcut
|
|
# into Shopfloor Tools\. Folder creation is deferred until we know we
|
|
# have at least one app to put in it (lazy creation, so PC types with
|
|
# nothing installed don't get an empty Shopfloor Tools folder).
|
|
#
|
|
# Kind = 'exe' -> build a fresh .lnk from ExePath
|
|
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
|
|
$cfgApps = Get-ProfileValue 'desktopApps'
|
|
|
|
if ($null -ne $cfgApps -and $cfgApps.Count -gt 0) {
|
|
$apps = @($cfgApps | ForEach-Object {
|
|
$entry = @{ Name = $_.name; Kind = $_.kind }
|
|
if ($_.kind -eq 'exe') { $entry.ExePath = $_.exePath }
|
|
if ($_.kind -eq 'existing') { $entry.SourceName = $_.sourceName }
|
|
$entry
|
|
})
|
|
} else {
|
|
$apps = @(
|
|
@{ Name = 'UDC'; Kind = 'exe'; ExePath = 'C:\Program Files\UDC\UDC.exe' }
|
|
@{ Name = 'eDNC'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\bin\DncMain.exe' }
|
|
@{ Name = 'NTLARS'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\Common\NTLARS.exe' }
|
|
@{ Name = 'WJ Shopfloor'; Kind = 'existing'; SourceName = 'WJ Shopfloor.lnk' }
|
|
@{ Name = 'Defect_Tracker'; Kind = 'existing'; SourceName = 'Defect_Tracker.lnk' }
|
|
)
|
|
}
|
|
|
|
# Lazy folder creation - only create Shopfloor Tools\ the first time
|
|
# we have an app that's actually going to land in it. PC types with
|
|
# nothing installed get no empty folder.
|
|
$ensureToolsDir = {
|
|
if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
|
|
try {
|
|
New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
|
|
return $true
|
|
} catch {
|
|
Write-Warning "Failed to create $shopfloorToolsDir : $_"
|
|
return $false
|
|
}
|
|
}
|
|
return $true
|
|
}
|
|
|
|
foreach ($app in $apps) {
|
|
$dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"
|
|
|
|
switch ($app.Kind) {
|
|
'exe' {
|
|
if (Test-Path -LiteralPath $app.ExePath) {
|
|
if (-not (& $ensureToolsDir)) { break }
|
|
if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) {
|
|
Write-Host " created: $($app.Name) -> $dest"
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($app.Name) - target not installed ($($app.ExePath))" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
|
|
'existing' {
|
|
$src = Find-ExistingLnk $app.SourceName
|
|
if ($src) {
|
|
# Skip if the sweep already moved the source into the
|
|
# destination folder (src == dest -> "cannot overwrite
|
|
# the item with itself").
|
|
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
|
|
(Join-Path $shopfloorToolsDir $app.SourceName)) {
|
|
Write-Host " exists: $($app.Name) (already in Shopfloor Tools)"
|
|
} else {
|
|
if (-not (& $ensureToolsDir)) { break }
|
|
try {
|
|
Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop
|
|
Write-Host " copied: $($app.Name) from $src"
|
|
} catch {
|
|
Write-Warning "Failed to copy $src -> $dest : $_"
|
|
}
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($app.Name) - no source .lnk found" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 3: Remove empty category folders
|
|
#
|
|
# If the sweep classified nothing into a given category (e.g. no Office
|
|
# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an
|
|
# empty folder cluttering the desktop. The scheduled-task sweep also runs
|
|
# this so categories that go empty between logons self-heal.
|
|
# ============================================================================
|
|
function Remove-EmptyCategoryFolders {
|
|
foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) {
|
|
$dir = Join-Path $publicDesktop $cat
|
|
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
|
$contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
|
|
if ($contents.Count -eq 0) {
|
|
try {
|
|
Remove-Item -LiteralPath $dir -Force -ErrorAction Stop
|
|
Write-Host " removed empty: $cat\"
|
|
} catch {
|
|
Write-Warning "Failed to remove empty $dir : $_"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Phase 4: Edge default browser + startup tabs (delegated to 08)
|
|
#
|
|
# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on
|
|
# the Public Desktop (or in Web Links\ after the sweep). The .url files
|
|
# are delivered by DSC AFTER this initial shopfloor setup, so the first
|
|
# run at imaging time won't find them (falls back to hardcoded URLs,
|
|
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
|
|
# scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
|
|
# the .url files and the next sweep files them into Web Links\, 08
|
|
# picks them up and updates the Edge policy. Self-healing.
|
|
# ============================================================================
|
|
function Invoke-EdgeDefaultBrowser {
|
|
$edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1'
|
|
if (-not (Test-Path -LiteralPath $edgeScript)) {
|
|
Write-Host " 08-EdgeDefaultBrowser.ps1 not found - skipping"
|
|
return
|
|
}
|
|
try {
|
|
& $edgeScript
|
|
} catch {
|
|
Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Scheduled task registration (re-runs 06 at every logon as SYSTEM)
|
|
# ============================================================================
|
|
function Register-SweepScheduledTask {
|
|
param([string]$ScriptPath)
|
|
|
|
$taskName = 'Organize Public Desktop'
|
|
|
|
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
|
Write-Warning "Cannot register scheduled task - script not found at $ScriptPath"
|
|
return
|
|
}
|
|
|
|
try {
|
|
$action = New-ScheduledTaskAction `
|
|
-Execute 'powershell.exe' `
|
|
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
|
|
|
|
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
|
|
|
# Run as SYSTEM so the script can (a) pass its admin check at the
|
|
# top, (b) write to Public Desktop / ProgramData Start Menu / the
|
|
# Default User profile without Access Denied, and (c) register the
|
|
# next iteration of itself idempotently. Earlier versions used
|
|
# -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon
|
|
# but the script exited immediately on the non-admin check.
|
|
$principal = New-ScheduledTaskPrincipal `
|
|
-UserId 'NT AUTHORITY\SYSTEM' `
|
|
-LogonType ServiceAccount `
|
|
-RunLevel Highest
|
|
|
|
$settings = New-ScheduledTaskSettingsSet `
|
|
-AllowStartIfOnBatteries `
|
|
-DontStopIfGoingOnBatteries `
|
|
-StartWhenAvailable `
|
|
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
|
|
|
# -ErrorAction Stop is required because Register-ScheduledTask emits
|
|
# non-terminating errors on failure (Access Denied etc), which try/catch
|
|
# would otherwise miss and we'd falsely claim success.
|
|
Register-ScheduledTask `
|
|
-TaskName $taskName `
|
|
-Action $action `
|
|
-Trigger $trigger `
|
|
-Principal $principal `
|
|
-Settings $settings `
|
|
-Force `
|
|
-ErrorAction Stop | Out-Null
|
|
|
|
Write-Host "Scheduled task '$taskName' registered (runs at every logon)."
|
|
} catch {
|
|
Write-Warning "Failed to register scheduled task : $_"
|
|
}
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
Write-Host ""
|
|
Write-Host "=== Phase 0: create Office shortcuts (if Office is installed) ==="
|
|
# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but
|
|
# doesn't finish streaming until AFTER the first reboot, so the first
|
|
# imaging-time run of 06 finds no Office and this no-ops. On the second
|
|
# logon (after reboot), Office is installed and 05 creates the shortcuts
|
|
# so Phase 1 below can sweep them into the Office\ folder.
|
|
$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1'
|
|
if (Test-Path -LiteralPath $officeScript) {
|
|
try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" }
|
|
} else {
|
|
Write-Host " 05-OfficeShortcuts.ps1 not found - skipping"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 1: sweep loose shortcuts into category folders ==="
|
|
Invoke-DesktopSweep -DesktopPath $publicDesktop
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ==="
|
|
Add-ShopfloorToolsApps
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 3: auto-apply startup items from PC profile ==="
|
|
# Creates .lnk files in AllUsers Startup folder based on the profile's
|
|
# startupItems. This runs as SYSTEM at every logon via the sweep scheduled
|
|
# task, so any item the tech removes via Configure-PC would bounce right
|
|
# back unless we remember the removal. startup-overrides.json tracks labels
|
|
# the tech has explicitly suppressed; items in that list are skipped here
|
|
# and re-enabling them via Configure-PC removes them from the list.
|
|
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
|
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
|
|
|
$suppressed = @()
|
|
if (Test-Path -LiteralPath $overridesPath) {
|
|
try {
|
|
$overrides = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
|
if ($overrides.suppressed) { $suppressed = @($overrides.suppressed) }
|
|
} catch {
|
|
Write-Warning " Failed to parse $overridesPath : $_"
|
|
}
|
|
}
|
|
|
|
$cfgStartup = Get-ProfileValue 'startupItems'
|
|
|
|
if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
|
|
if (-not (Test-Path $startupDir)) {
|
|
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
|
|
}
|
|
$edgePath = @(
|
|
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
|
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
|
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
|
|
|
foreach ($si in $cfgStartup) {
|
|
if ($suppressed -contains $si.label) {
|
|
Write-Host " suppressed: $($si.label) (tech disabled via Configure-PC)"
|
|
continue
|
|
}
|
|
$lnkPath = Join-Path $startupDir "$($si.label).lnk"
|
|
# Skip if already exists (idempotent - don't overwrite user changes)
|
|
if (Test-Path -LiteralPath $lnkPath) {
|
|
Write-Host " exists: $($si.label)"
|
|
continue
|
|
}
|
|
switch ($si.type) {
|
|
'exe' {
|
|
if (Test-Path -LiteralPath $si.target) {
|
|
if (New-ShopfloorLnk -Path $lnkPath -Target $si.target) {
|
|
Write-Host " added: $($si.label)"
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($si.label) - target not installed" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
'existing' {
|
|
$src = Find-ExistingLnk $si.sourceLnk
|
|
if ($src) {
|
|
try {
|
|
Copy-Item -LiteralPath $src -Destination $lnkPath -Force
|
|
Write-Host " added: $($si.label)"
|
|
} catch { Write-Warning " failed: $($si.label) - $_" }
|
|
} else {
|
|
Write-Host " skip: $($si.label) - source not found" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
'url' {
|
|
$url = $null
|
|
if ($si.urlKey -and $siteConfig -and $siteConfig.urls) {
|
|
$url = $siteConfig.urls.$($si.urlKey)
|
|
}
|
|
if ($url -and $edgePath) {
|
|
if (New-ShopfloorLnk -Path $lnkPath -Target $edgePath -Arguments "--new-window `"$url`"") {
|
|
Write-Host " added: $($si.label)"
|
|
}
|
|
} else {
|
|
Write-Host " skip: $($si.label) - URL or Edge not available" -ForegroundColor DarkGray
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
Write-Host " (no startup items in profile)"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 4: remove empty category folders ==="
|
|
Remove-EmptyCategoryFolders
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 5: Edge default browser + startup tabs ==="
|
|
Invoke-EdgeDefaultBrowser
|
|
|
|
Write-Host ""
|
|
Write-Host "=== Phase 6: register logon sweeper scheduled task ==="
|
|
Register-SweepScheduledTask -ScriptPath $scriptPath
|
|
|
|
Write-Host ""
|
|
Write-Host "=== 06-OrganizeDesktop session end ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
|
try { Stop-Transcript | Out-Null } catch {}
|
|
exit 0
|