Demo: migrations follow the schedule, worked by a simulated team
Each device now migrates on the day of its date tag instead of on a looping 24-hour clock: earlier days are finished, today's devices go through backup and restore during working hours, later days haven't started. The few that don't finish have a cause the dashboard shows (broken client, broken WMI, low disk, a USMT error, unreachable PC). A simulated team keeps two weeks of working days tagged. During working hours it adds devices, reschedules or drops one, tags stuck PCs with their problem, then fixes them and moves them to a new day; each morning finished days are closed out and a new day is scheduled. Actions print in the worker terminal as [Team] lines. Settings live in Demo.Team. The visitor sandbox now keeps per-tag edits laid over the live roster, so the team's changes keep showing around a visitor's own edits. Overview calendar: today is a filled badge on the date number, so it no longer looks like the selected day (outline plus tint).
This commit is contained in:
7 files changed
+563
-155
No files matched your search
+453
-90
@@ -3,9 +3,12 @@
|
||||
#
|
||||
# Implements the same functions as Providers\SCCM.ps1, but every answer comes from a
|
||||
# deterministic model instead of SCCM / AD / DNS / SMB. A device's hardware, users,
|
||||
# location and software are derived from a hash of its name, and its migration moves
|
||||
# through each stage on a clock, so the unmodified worker loop sees devices go from
|
||||
# Not Started -> Backup In Progress -> Backup Completed -> Restore In Progress -> Completed.
|
||||
# location and software are derived from a hash of its name. Its migration happens on the
|
||||
# day of its date tag (10/14 and so on), so the unmodified worker loop sees that day's devices
|
||||
# go Not Started -> Backup In Progress -> Backup Completed -> Restore In Progress -> Completed,
|
||||
# earlier days finished (apart from a few with a real reason, such as a broken client or low
|
||||
# disk) and later days not started yet. A simulated team keeps the date tags moving: see
|
||||
# Invoke-DemoTeam at the end of this file.
|
||||
#
|
||||
# Naming scheme: PrvDT- / PubDT- (private/public desktops) PrvLT- / PubLT- (laptops)
|
||||
# followed by an 8 character serial. Destination (replacement) devices get a serial that
|
||||
@@ -13,20 +16,14 @@
|
||||
# =========================================================================================
|
||||
|
||||
$script:DemoCfg = $config.Demo
|
||||
$script:DemoLifecycle = [double]$script:DemoCfg.LifecycleHours * 3600
|
||||
$script:DemoTeamCfg = $script:DemoCfg.Team
|
||||
$script:DemoDataDir = Join-Path (Split-Path $PSScriptRoot -Parent) "Data"
|
||||
$script:DemoProfiles = @{}
|
||||
$script:DemoAlphabet = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"
|
||||
$script:DemoAdminShare = 'C$'
|
||||
$script:DemoLogFolders = @('Windows\CCM\Logs\SMSTSLog', 'Windows\CCM\Logs')
|
||||
|
||||
# --- Stage boundaries (fraction of the lifecycle) ---
|
||||
$script:DemoStages = @(
|
||||
@{ Name = 'NotStarted'; Start = 0.00 }
|
||||
@{ Name = 'BackupInProgress'; Start = 0.25 }
|
||||
@{ Name = 'BackupCompleted'; Start = 0.40 }
|
||||
@{ Name = 'RestoreInProgress'; Start = 0.55 }
|
||||
@{ Name = 'Completed'; Start = 0.70 }
|
||||
)
|
||||
$script:DemoStageOrder = @('NotStarted', 'BackupInProgress', 'BackupCompleted', 'RestoreInProgress', 'Completed')
|
||||
|
||||
# --- Catalogs ---
|
||||
$script:DemoSurnames = @("Smith","Johnson","Williams","Brown","Jones","Garcia","Miller","Davis","Rodriguez","Martinez","Hernandez","Lopez","Gonzalez","Wilson","Anderson","Thomas","Taylor","Moore","Jackson","Martin","Lee","Perez","Thompson","White","Harris","Sanchez","Clark","Ramirez","Lewis","Robinson","Walker","Young","Allen","King","Wright","Scott","Torres","Nguyen","Hill","Flores","Green","Adams","Nelson","Baker","Hall","Rivera","Campbell","Mitchell","Carter","Roberts","Patel","Kim","Chen","Shah","Murphy","Cook","Rogers","Morgan","Cooper","Reed")
|
||||
@@ -135,42 +132,6 @@ function Get-DemoPick([string]$name, [string]$salt, [array]$list) { return $list
|
||||
function Get-DemoInt([string]$name, [string]$salt, [int]$min, [int]$max) { return $min + [int][math]::Floor((Get-DemoRand $name $salt) * ($max - $min + 1)) }
|
||||
function Get-DemoNow { return [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() }
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Date-batch tags ("M/d") sit on fixed working-day offsets from today. The seed records the
|
||||
# labels it used in Demo-Dates.json and the worker moves them along when the day changes, so
|
||||
# a demo container that runs for weeks still shows this week's deployments.
|
||||
# -----------------------------------------------------------------------------------------
|
||||
$script:DemoDateOffsets = @(-4, -3, -2, -1, 0, 1)
|
||||
function Get-DemoWorkday([datetime]$from, [int]$offset) {
|
||||
$d = $from.Date; $step = [math]::Sign($offset); $left = [math]::Abs($offset)
|
||||
while ($left -gt 0) { $d = $d.AddDays($step); if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $left-- } }
|
||||
return $d
|
||||
}
|
||||
function Get-DemoDateTags { $today = Get-Date; return @($script:DemoDateOffsets | ForEach-Object { (Get-DemoWorkday $today $_).ToString("M/d") }) }
|
||||
|
||||
function Update-DemoDateTags([string]$DataDir) {
|
||||
$metaPath = Join-Path $DataDir "Demo-Dates.json"
|
||||
$trackedPath = Join-Path $DataDir "Tracked-Devices.json"
|
||||
if (-not (Test-Path $metaPath) -or -not (Test-Path $trackedPath)) { return }
|
||||
try { $old = @(([System.IO.File]::ReadAllText($metaPath) | ConvertFrom-Json).Tags) } catch { return }
|
||||
$new = Get-DemoDateTags
|
||||
if ($old.Count -ne $new.Count -or ($old -join '|') -eq ($new -join '|')) { return }
|
||||
|
||||
$rename = @{}
|
||||
for ($i = 0; $i -lt $old.Count; $i++) { $rename[$old[$i]] = $new[$i] }
|
||||
try { $tracked = [System.IO.File]::ReadAllText($trackedPath) | ConvertFrom-Json } catch { return }
|
||||
$out = [ordered]@{}
|
||||
foreach ($prop in $tracked.PSObject.Properties) {
|
||||
$name = if ($rename.ContainsKey($prop.Name)) { $rename[$prop.Name] } else { $prop.Name }
|
||||
$out[$name] = $prop.Value
|
||||
}
|
||||
$tmp = "$trackedPath.tmp"
|
||||
[System.IO.File]::WriteAllText($tmp, ($out | ConvertTo-Json -Depth 5), [System.Text.Encoding]::UTF8)
|
||||
Move-Item -LiteralPath $tmp -Destination $trackedPath -Force
|
||||
[System.IO.File]::WriteAllText($metaPath, (@{ Tags = $new } | ConvertTo-Json), [System.Text.Encoding]::UTF8)
|
||||
Write-Host "Demo: date tags moved to today ($($old -join ', ') -> $($new -join ', '))" -ForegroundColor DarkCyan
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Serial scheme: 7 chars + check char. Sources and destinations use different check offsets
|
||||
# and a destination's first 7 chars are a reversible scramble of its source's.
|
||||
@@ -219,9 +180,10 @@ function Get-DemoProfile([string]$name) {
|
||||
|
||||
# Facts shared by a source and its replacement come from the SOURCE name
|
||||
$s = $p.Source
|
||||
$p.Offset = (Get-DemoRand $s 'offset') * $script:DemoLifecycle
|
||||
$p.NoAssoc = (Get-DemoRand $s 'assoc') -lt 0.08
|
||||
$p.Failing = (Get-DemoRand $s 'fail') -lt 0.05
|
||||
# Problems that stop a migration on its day (until the team fixes them)
|
||||
$p.LowDisk = (Get-DemoRand $s 'lowdisk') -lt 0.04
|
||||
$p.BrokenWmi = (Get-DemoRand $s 'wmi') -lt 0.02
|
||||
$p.Vendor = Get-DemoPick $s 'vendor' @("HP", "HP", "Dell", "Lenovo")
|
||||
$p.Building = Get-DemoPick $s 'bldg' $script:DemoBuildings
|
||||
$p.Department = Get-DemoPick $s 'dept' $script:DemoDepartments
|
||||
@@ -236,11 +198,12 @@ function Get-DemoProfile([string]$name) {
|
||||
$p.User = ("abcdefghijklmnoprstw"[(Get-DemoInt $s 'fi' 0 19)]).ToString() + (Get-DemoPick $s 'sn' $script:DemoSurnames).ToLower()
|
||||
$p.InfoBy = (Get-DemoPick $s 'ib1' @("A","B","C","D","J","K","M","R","S","T")) + (Get-DemoPick $s 'ib2' @("B","C","G","H","L","M","P","R","S","W"))
|
||||
$p.Notes = Get-DemoPick $s 'notes' $script:DemoNotes
|
||||
$p.SrcFree = if ((Get-DemoRand $s 'lowdisk') -lt 0.08) { 4.0 + 12 * (Get-DemoRand $s 'freeL') } else { 30.0 + 150 * (Get-DemoRand $s 'free') }
|
||||
$p.SrcFree = 30.0 + 150 * (Get-DemoRand $s 'free')
|
||||
$p.LowFree = 3.0 + 9 * (Get-DemoRand $s 'freeL')
|
||||
$p.Apps = @($p.Department.Apps | Where-Object { (Get-DemoRand $s "app$_") -lt 0.65 })
|
||||
if ($p.Apps.Count -eq 0) { $p.Apps = @($p.Department.Apps[0]) }
|
||||
# A few old devices have been unplugged / powered off for days (not while they're mid-migration)
|
||||
$p.OfflineDays = if ((Get-DemoRand $s 'longoff') -lt 0.07) { Get-DemoInt $s 'offdays' 1 21 } else { 0 }
|
||||
# A few old devices have been unplugged / powered off for days
|
||||
$p.OfflineDays = if ((Get-DemoRand $s 'longoff') -lt 0.03) { Get-DemoInt $s 'offdays' 1 21 } else { 0 }
|
||||
# Monitors at the desk move to the replacement, so both share the source's set
|
||||
$monCount = if ($p.Laptop) { Get-DemoPick $s 'mons' @(0, 1, 1, 2) } else { Get-DemoPick $s 'mons' @(1, 2, 2, 2, 3) }
|
||||
$p.Monitors = @(1..3 | Select-Object -First $monCount | ForEach-Object { Get-DemoPick $s "mon$(if ((Get-DemoRand $s 'monmatch') -lt 0.7) { 1 } else { $_ })" $script:DemoMonitors })
|
||||
@@ -256,30 +219,149 @@ function Get-DemoProfile([string]$name) {
|
||||
$p.DiskModel = Get-DemoPick $n 'ssd' $script:DemoSsdModels
|
||||
$p.DiskSerial = (1..8 | ForEach-Object { "{0:X4}" -f (Get-DemoInt $n "dsn$_" 0 65535) }) -join '_'
|
||||
$p.Inactive = (Get-DemoRand $n 'inactive') -lt 0.04
|
||||
$p.NoSccm = (-not $p.IsDest) -and (Get-DemoRand $n 'nosccm') -lt 0.025 # broken client, never reported
|
||||
$p.NoSccm = (-not $p.IsDest) -and (Get-DemoRand $n 'nosccm') -lt 0.02 # broken client, never reported
|
||||
$p.NoAd = (-not $p.IsDest) -and (Get-DemoRand $n 'noad') -lt 0.012 # computer object deleted
|
||||
}
|
||||
$script:DemoProfiles[$key] = $p
|
||||
return $p
|
||||
}
|
||||
|
||||
# Where a migration currently is. Both halves of a pair share the source's clock.
|
||||
function Get-DemoStage([hashtable]$p) {
|
||||
$now = Get-DemoNow
|
||||
$L = $script:DemoLifecycle
|
||||
$phase = (($now + $p.Offset) % $L) / $L
|
||||
$idx = 0
|
||||
for ($i = 0; $i -lt $script:DemoStages.Count; $i++) { if ($phase -ge $script:DemoStages[$i].Start) { $idx = $i } }
|
||||
$start = $script:DemoStages[$idx].Start
|
||||
$end = if ($idx -lt $script:DemoStages.Count - 1) { $script:DemoStages[$idx + 1].Start } else { 1.0 }
|
||||
$cycleStart = $now - ($phase * $L)
|
||||
return @{
|
||||
Name = $script:DemoStages[$idx].Name
|
||||
Phase = $phase
|
||||
Progress = ($phase - $start) / ($end - $start)
|
||||
StageStarted = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $start * $L)).LocalDateTime
|
||||
StageEnded = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $end * $L)).LocalDateTime
|
||||
# -----------------------------------------------------------------------------------------
|
||||
# Schedule: each source device migrates on the day of its date tag
|
||||
# -----------------------------------------------------------------------------------------
|
||||
function Test-DemoWorkday([datetime]$d) { return @($script:DemoTeamCfg.Weekdays) -contains [int]$d.DayOfWeek }
|
||||
function Get-DemoWorkday([datetime]$from, [int]$offset) {
|
||||
# $offset working days from $from (0 = $from itself if it's a working day, else the next one)
|
||||
$d = $from.Date
|
||||
if ($offset -ge 0) {
|
||||
while (-not (Test-DemoWorkday $d)) { $d = $d.AddDays(1) }
|
||||
for ($i = 0; $i -lt $offset; $i++) { do { $d = $d.AddDays(1) } until (Test-DemoWorkday $d) }
|
||||
} else {
|
||||
for ($i = 0; $i -lt -$offset; $i++) { do { $d = $d.AddDays(-1) } until (Test-DemoWorkday $d) }
|
||||
}
|
||||
return $d
|
||||
}
|
||||
function ConvertFrom-DemoDateTag([string]$tag) {
|
||||
if ($tag -notmatch '^(\d{1,2})/(\d{1,2})$') { return $null }
|
||||
$today = (Get-Date).Date
|
||||
try { $d = [datetime]::new($today.Year, [int]$Matches[1], [int]$Matches[2]) } catch { return $null }
|
||||
if (($d - $today).TotalDays -gt 183) { $d = $d.AddYears(-1) } elseif (($today - $d).TotalDays -gt 183) { $d = $d.AddYears(1) }
|
||||
return $d
|
||||
}
|
||||
function ConvertTo-DemoDateTag([datetime]$d) { return $d.ToString("M/d", [System.Globalization.CultureInfo]::InvariantCulture) }
|
||||
|
||||
# Tags as a list of @{ Name; Devices; Skip } from a parsed Tracked-Devices.json (object or hashtable)
|
||||
function Get-DemoTagList($tracked) {
|
||||
$out = [System.Collections.Generic.List[object]]::new()
|
||||
if (-not $tracked) { return $out }
|
||||
$pairs = if ($tracked -is [System.Collections.IDictionary]) { $tracked.GetEnumerator() | ForEach-Object { @{ Name = $_.Key; Value = $_.Value } } }
|
||||
else { $tracked.PSObject.Properties | ForEach-Object { @{ Name = $_.Name; Value = $_.Value } } }
|
||||
foreach ($pair in $pairs) {
|
||||
$v = $pair.Value
|
||||
$devs = if ($v -is [System.Collections.IDictionary]) { $v.devices } elseif ($v.PSObject.Properties.Name -contains 'devices') { $v.devices } else { $v }
|
||||
$skip = if ($v -is [System.Collections.IDictionary]) { [bool]$v.skipUSMT } elseif ($v.PSObject.Properties.Name -contains 'skipUSMT') { [bool]$v.skipUSMT } else { $false }
|
||||
$out.Add(@{ Name = $pair.Name; Devices = @($devs | Where-Object { $_ } | ForEach-Object { "$_".Trim().ToUpper() }); Skip = $skip })
|
||||
}
|
||||
return $out
|
||||
}
|
||||
|
||||
$script:DemoSchedule = $null # source -> scheduled day
|
||||
$script:DemoFixed = [System.Collections.Generic.HashSet[string]]::new() # problems the team has fixed
|
||||
$script:DemoPlans = @{}
|
||||
$script:DemoScheduleStamp = ''
|
||||
$script:DemoScheduleChecked = [datetime]::MinValue
|
||||
|
||||
# -Pin: use this roster as is (the seed and the team work on one in memory), don't re-read the file
|
||||
function Set-DemoSchedule($tracked, $fixed, [switch]$Pin) {
|
||||
$sched = @{}
|
||||
foreach ($t in (Get-DemoTagList $tracked)) {
|
||||
$day = ConvertFrom-DemoDateTag $t.Name
|
||||
if (-not $day) { continue }
|
||||
foreach ($d in $t.Devices) { if ($d -notmatch ',' -and (-not $sched.ContainsKey($d) -or $sched[$d] -lt $day)) { $sched[$d] = $day } }
|
||||
}
|
||||
$script:DemoSchedule = $sched
|
||||
$script:DemoFixed = [System.Collections.Generic.HashSet[string]]::new([string[]]@($fixed | Where-Object { $_ }))
|
||||
$script:DemoPlans = @{}
|
||||
$script:DemoSchedulePinned = [bool]$Pin
|
||||
}
|
||||
|
||||
# Re-reads the roster (and the team's fixes) when they change on disk; checked at most every few seconds
|
||||
$script:DemoSchedulePinned = $false
|
||||
function Import-DemoSchedule {
|
||||
if ($script:DemoSchedulePinned) { return }
|
||||
if ($script:DemoSchedule -and ((Get-Date) - $script:DemoScheduleChecked).TotalSeconds -lt 3) { return }
|
||||
$script:DemoScheduleChecked = Get-Date
|
||||
$tp = Join-Path $script:DemoDataDir "Tracked-Devices.json"
|
||||
$fp = Join-Path $script:DemoDataDir "Demo-Team.json"
|
||||
$stamp = (@($tp, $fp) | ForEach-Object { if (Test-Path $_) { (Get-Item $_).LastWriteTimeUtc.Ticks } else { 0 } }) -join '|'
|
||||
if ($script:DemoSchedule -and $stamp -eq $script:DemoScheduleStamp) { return }
|
||||
$tracked = $null; $fixed = @()
|
||||
try { $tracked = [System.IO.File]::ReadAllText($tp) | ConvertFrom-Json } catch {}
|
||||
try { $fixed = @(([System.IO.File]::ReadAllText($fp) | ConvertFrom-Json).Fixed) } catch {}
|
||||
Set-DemoSchedule $tracked $fixed
|
||||
$script:DemoScheduleStamp = $stamp
|
||||
}
|
||||
|
||||
# What the problems that block a device are, before the team fixes them
|
||||
function Test-DemoOffline([hashtable]$sp) { return $sp.OfflineDays -gt 0 -and -not $script:DemoFixed.Contains($sp.Name) }
|
||||
|
||||
# The day's timeline for a source: when backup and restore run and whether something stops it
|
||||
function Get-DemoPlan([string]$source) {
|
||||
Import-DemoSchedule
|
||||
$day = $script:DemoSchedule[$source]
|
||||
if (-not $day) { return $null }
|
||||
$d = $day.ToString('yyyyMMdd')
|
||||
$key = "$source|$d"
|
||||
if ($script:DemoPlans.ContainsKey($key)) { return $script:DemoPlans[$key] }
|
||||
|
||||
$sp = Get-DemoProfile $source
|
||||
$ws = [timespan]$script:DemoTeamCfg.WorkStart; $we = [timespan]$script:DemoTeamCfg.WorkEnd
|
||||
$room = [math]::Max(30, ($we - $ws).TotalMinutes - 150) # last ones still finish around closing time
|
||||
$bStart = $day.Add($ws).AddMinutes([math]::Floor((Get-DemoRand $source "start$d") * $room))
|
||||
$bEnd = $bStart.AddMinutes((Get-DemoInt $source "bdur$d" 30 70))
|
||||
$rStart = $bEnd.AddMinutes((Get-DemoInt $source "swap$d" 15 60)) # hardware swap at the desk
|
||||
$rEnd = $rStart.AddMinutes((Get-DemoInt $source "rdur$d" 20 45))
|
||||
|
||||
$fail = $null
|
||||
if (-not $script:DemoFixed.Contains($source)) {
|
||||
$r = Get-DemoRand $source "err$d"
|
||||
if ($sp.NoSccm) { $fail = 'BadClient' }
|
||||
elseif (Test-DemoOffline $sp) { $fail = 'Offline' }
|
||||
elseif ($sp.BrokenWmi) { $fail = 'WMI' }
|
||||
elseif ($sp.LowDisk) { $fail = 'LowDisk' }
|
||||
elseif ($r -lt 0.025) { $fail = 'BackupError' }
|
||||
elseif ($r -lt 0.035) { $fail = 'RestoreError' }
|
||||
}
|
||||
$failAt = switch ($fail) {
|
||||
{ $_ -in 'LowDisk', 'BackupError' } { $bStart.AddMinutes(0.55 * ($bEnd - $bStart).TotalMinutes); break }
|
||||
'RestoreError' { $rStart.AddMinutes(0.6 * ($rEnd - $rStart).TotalMinutes); break }
|
||||
$null { $null; break }
|
||||
default { $bStart } # can't even start: the task sequence never runs
|
||||
}
|
||||
$plan = @{ Day = $day; BackupStart = $bStart; BackupEnd = $bEnd; RestoreStart = $rStart; RestoreEnd = $rEnd; Fail = $fail; FailAt = $failAt }
|
||||
$script:DemoPlans[$key] = $plan
|
||||
return $plan
|
||||
}
|
||||
|
||||
# Where a migration is right now. Both halves of a pair share the source's plan.
|
||||
function Get-DemoStage([hashtable]$p) {
|
||||
$plan = Get-DemoPlan $p.Source
|
||||
$now = Get-Date
|
||||
$st = { param($name, $progress, $from, $to, $failed) @{ Name = $name; Progress = $progress; StageStarted = $from; StageEnded = $to; Plan = $plan; Failed = [bool]$failed } }
|
||||
if (-not $plan) { return & $st 'NotStarted' 0 $now.Date $now.Date $false } # not scheduled
|
||||
if ($plan.Fail -in 'BadClient', 'Offline', 'WMI') { return & $st 'NotStarted' 0 $plan.Day $plan.Day $false }
|
||||
if ($now -lt $plan.BackupStart) { return & $st 'NotStarted' 0 $plan.Day $plan.BackupStart $false }
|
||||
$backupFails = $plan.Fail -in 'LowDisk', 'BackupError'
|
||||
if ($now -lt $plan.BackupEnd -or $backupFails) {
|
||||
if ($backupFails -and $now -ge $plan.FailAt) { return & $st 'BackupInProgress' 0.55 $plan.BackupStart $plan.BackupEnd $true }
|
||||
return & $st 'BackupInProgress' (($now - $plan.BackupStart).TotalSeconds / ($plan.BackupEnd - $plan.BackupStart).TotalSeconds) $plan.BackupStart $plan.BackupEnd $false
|
||||
}
|
||||
if ($now -lt $plan.RestoreStart) { return & $st 'BackupCompleted' 0 $plan.BackupEnd $plan.RestoreStart $false }
|
||||
if ($now -lt $plan.RestoreEnd -or $plan.Fail -eq 'RestoreError') {
|
||||
if ($plan.Fail -eq 'RestoreError' -and $now -ge $plan.FailAt) { return & $st 'RestoreInProgress' 0.6 $plan.RestoreStart $plan.RestoreEnd $true }
|
||||
return & $st 'RestoreInProgress' (($now - $plan.RestoreStart).TotalSeconds / ($plan.RestoreEnd - $plan.RestoreStart).TotalSeconds) $plan.RestoreStart $plan.RestoreEnd $false
|
||||
}
|
||||
return & $st 'Completed' 1 $plan.RestoreEnd $plan.RestoreEnd $false
|
||||
}
|
||||
|
||||
function Get-DemoNetworkState([string]$computer) {
|
||||
@@ -291,7 +373,7 @@ function Get-DemoNetworkState([string]$computer) {
|
||||
if ($fault -lt 0.03) { return "DNS Issue" }
|
||||
|
||||
$stage = (Get-DemoStage $p).Name
|
||||
if (-not $p.IsDest -and $p.OfflineDays -gt 0 -and $stage -in @('NotStarted', 'Completed')) { return "Timed Out" }
|
||||
if (-not $p.IsDest -and (Test-DemoOffline $p)) { return "Timed Out" }
|
||||
if ($p.IsDest) {
|
||||
if (Test-DemoNotImaged $p) { return "Not Found" } # not imaged yet: no DNS record
|
||||
if ($stage -eq 'RestoreInProgress') { return "Online" }
|
||||
@@ -308,7 +390,12 @@ function Get-DemoNetworkState([string]$computer) {
|
||||
|
||||
# A replacement that hasn't been imaged yet isn't in DNS, AD or SCCM
|
||||
function Test-DemoNotImaged([hashtable]$p) {
|
||||
return $p.IsDest -and (Get-DemoStage $p).Name -eq 'NotStarted' -and (Get-DemoRand $p.Name 'staged') -lt 0.5
|
||||
if (-not $p.IsDest) { return $false }
|
||||
$stage = Get-DemoStage $p
|
||||
if ($stage.Name -ne 'NotStarted') { return $false }
|
||||
# Replacements are imaged a few days ahead of their date
|
||||
if (-not $stage.Plan -or ($stage.Plan.Day - (Get-Date).Date).TotalDays -gt 3) { return $true }
|
||||
return (Get-DemoRand $p.Name 'staged') -lt 0.2
|
||||
}
|
||||
|
||||
function Start-DemoLatency([int]$scale = 1) {
|
||||
@@ -367,7 +454,9 @@ function Get-DiskSpace {
|
||||
# A small drift so free space isn't frozen between cycles
|
||||
$drift = 2 * (Get-DemoRand $p.Name "drift$([math]::Floor((Get-DemoNow) / 3600))") - 1
|
||||
$total = [math]::Round($p.DiskSize * 0.93, 1)
|
||||
$free = [math]::Round([math]::Max(1.2, $p.SrcFree + $drift), 1)
|
||||
$sp = Get-DemoProfile $p.Source
|
||||
$base = if ($sp.LowDisk -and -not $script:DemoFixed.Contains($sp.Name)) { $sp.LowFree } else { $p.SrcFree }
|
||||
$free = [math]::Round([math]::Max(1.2, $base + $drift), 1)
|
||||
if ($v) { Write-Host "${free}GB Free out of ${total}GB" -ForegroundColor Green }
|
||||
return @{ Total = "$total GB"; Free = "$free GB" }
|
||||
}
|
||||
@@ -394,31 +483,36 @@ function Get-LogInfo {
|
||||
|
||||
if ($p.Valid) {
|
||||
$stage = Get-DemoStage $p
|
||||
$plan = $stage.Plan
|
||||
$active = if ($isScan) { 'BackupInProgress' } else { 'RestoreInProgress' }
|
||||
$doneFrom = if ($isScan) { 0.40 } else { 0.70 }
|
||||
$doneFrom = if ($isScan) { 2 } else { 4 } # index in DemoStageOrder
|
||||
$now = Get-Date
|
||||
|
||||
if ($stage.Name -eq $active) {
|
||||
# Task sequence still running: log lives in SMSTSLog and is being written to
|
||||
$msg = if ($isScan -and $p.Failing -and $stage.Progress -gt 0.5) {
|
||||
'errorText, "Insufficient free space on the migration store"'
|
||||
# Task sequence still running (or stopped on an error): log lives in SMSTSLog
|
||||
if ($stage.Failed) {
|
||||
$err = switch ($plan.Fail) {
|
||||
'LowDisk' { 'Insufficient free space on the migration store' }
|
||||
'BackupError' { Get-DemoPick $p.Source 'berr' @('Failed to gather data: Access is denied. (0x80070005)', 'Return code 61: Processing stopped due to an I/O error', 'Return code 27: Invalid store path') }
|
||||
default { Get-DemoPick $p.Source 'rerr' @('Failed to apply settings: the profile is in use (0x80070020)', 'Return code 71: Unable to start. Run USMT with elevated privileges') }
|
||||
}
|
||||
$msg = "errorText, `"$err`""
|
||||
$modified = $plan.FailAt
|
||||
} else {
|
||||
$pct = [int][math]::Floor($stage.Progress * 100)
|
||||
if ($pct -lt 8) { "PHASE, Initializing" }
|
||||
$msg = if ($pct -lt 8) { "PHASE, Initializing" }
|
||||
elseif ($pct -lt 20) { if ($isScan) { "PHASE, Scanning" } else { "PHASE, Gathering" } }
|
||||
elseif ($pct -lt 35) { "detectedUser, CORP\$($p.User), includedInMigration, Yes" }
|
||||
elseif ($pct -lt 50) { if ($isScan) { "PHASE, Collecting" } else { "PHASE, Applying" } }
|
||||
else { "totalPercentageCompleted, $pct" }
|
||||
$modified = $now.AddSeconds(-1 * (Get-DemoInt $p.Name "lm$([math]::Floor((Get-DemoNow) / 60))" 20 240))
|
||||
if ($modified -lt $stage.StageStarted) { $modified = $stage.StageStarted }
|
||||
}
|
||||
$modified = $now.AddSeconds(-1 * (Get-DemoInt $p.Name "lm$([math]::Floor((Get-DemoNow) / 60))" 20 240))
|
||||
$found = @{ Folder = $script:DemoLogFolders[0]; Raw = (Format-DemoLogLine $modified $stage.StageStarted $msg); Modified = $modified }
|
||||
} elseif ($stage.Phase -ge $doneFrom) {
|
||||
} elseif ($plan -and $script:DemoStageOrder.IndexOf($stage.Name) -ge $doneFrom) {
|
||||
# Finished: the task sequence has ended and the log was moved to CCM\Logs
|
||||
$L = $script:DemoLifecycle
|
||||
$cycleStart = (Get-DemoNow) - ($stage.Phase * $L)
|
||||
$startFrac = if ($isScan) { 0.25 } else { 0.55 }
|
||||
$began = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $startFrac * $L)).LocalDateTime
|
||||
$ended = [DateTimeOffset]::FromUnixTimeSeconds([long]($cycleStart + $doneFrom * $L)).LocalDateTime
|
||||
$began = if ($isScan) { $plan.BackupStart } else { $plan.RestoreStart }
|
||||
$ended = if ($isScan) { $plan.BackupEnd } else { $plan.RestoreEnd }
|
||||
$found = @{ Folder = $script:DemoLogFolders[1]; Raw = (Format-DemoLogLine $ended $began "Successful run"); Modified = $ended }
|
||||
}
|
||||
}
|
||||
@@ -444,7 +538,12 @@ function Get-DeviceAssociations {
|
||||
param([string]$SourceComputer)
|
||||
Start-DemoLatency 2
|
||||
$p = Get-DemoProfile $SourceComputer
|
||||
if (-not $p.Valid -or $p.IsDest -or $p.NoAssoc) { return $null }
|
||||
if (-not $p.Valid -or $p.IsDest) { return $null }
|
||||
# Some associations are only created in SCCM a couple of days before the device's date
|
||||
if ($p.NoAssoc) {
|
||||
$plan = Get-DemoPlan $p.Name
|
||||
if (-not $plan -or ($plan.Day - (Get-Date).Date).TotalDays -gt 2) { return $null }
|
||||
}
|
||||
|
||||
$status = switch ((Get-DemoStage $p).Name) {
|
||||
'NotStarted' { 0 }
|
||||
@@ -464,7 +563,8 @@ function Get-DeviceInventory {
|
||||
$p = Get-DemoProfile $computerName
|
||||
if (-not $p.Valid) { return @{ InAD = $false; InSCCM = $false } } # a typo: nothing knows it
|
||||
if (Test-DemoNotImaged $p) { return @{ InAD = $false; InSCCM = $false } }
|
||||
$flags = @{ InAD = -not $p.NoAd; InSCCM = -not $p.NoSccm }
|
||||
$fixed = $script:DemoFixed.Contains($p.Name)
|
||||
$flags = @{ InAD = -not $p.NoAd; InSCCM = -not ($p.NoSccm -and -not $fixed) }
|
||||
|
||||
$vis = if ($p.Public) { "Public" } else { "Private" }
|
||||
$form = if ($p.Laptop) { "Laptops" } else { "Desktops" }
|
||||
@@ -497,7 +597,7 @@ function Get-DeviceInventory {
|
||||
RAM = $p.RAM
|
||||
InventoryIP = $p.IP
|
||||
LastUser = $lastUser
|
||||
ClientStatus = if ($p.Inactive) { "Installed / Inactive" } else { "Installed / Active" }
|
||||
ClientStatus = if ($p.Inactive -or ($p.BrokenWmi -and -not $fixed)) { "Installed / Inactive" } else { "Installed / Active" }
|
||||
Updated = $updated.ToString("M/d/yyyy")
|
||||
Location = $p.Location
|
||||
Vanity = $p.Vanity
|
||||
@@ -525,8 +625,8 @@ function Get-DeviceInventory {
|
||||
function Get-DeviceLastSeen {
|
||||
param([string]$computerName, $Session)
|
||||
$p = Get-DemoProfile $computerName
|
||||
if (-not $p.Valid -or (Test-DemoNotImaged $p) -or $p.NoSccm) { return $null }
|
||||
if (-not $p.IsDest -and $p.OfflineDays -gt 0) {
|
||||
if (-not $p.Valid -or (Test-DemoNotImaged $p) -or ($p.NoSccm -and -not $script:DemoFixed.Contains($p.Name))) { return $null }
|
||||
if (-not $p.IsDest -and (Test-DemoOffline $p)) {
|
||||
return (Get-Date).AddDays(-$p.OfflineDays).AddHours(-(Get-DemoInt $p.Name 'offhrs' 0 9))
|
||||
}
|
||||
return (Get-Date).AddMinutes(-(Get-DemoInt $p.Name "seen$([math]::Floor((Get-DemoNow) / 600))" 5 55))
|
||||
@@ -572,3 +672,266 @@ function Get-InstalledSoftware {
|
||||
}
|
||||
return $rows
|
||||
}
|
||||
|
||||
# =========================================================================================
|
||||
# Simulated team (demo worker only)
|
||||
#
|
||||
# Plays the IT team that owns the roster: the next two weeks of working days are tagged,
|
||||
# and during working hours someone adds devices, reschedules or drops one, tags a stuck PC
|
||||
# with its problem and later fixes it and moves it to a new day. Each morning finished days
|
||||
# are closed out (completed devices untracked, stragglers rescheduled) and a new day is
|
||||
# scheduled at the end. Every action is printed to the worker log.
|
||||
# =========================================================================================
|
||||
$script:DemoTechs = @("MR", "SK", "TB", "JC", "AL")
|
||||
$script:DemoIssueTags = @{ BadClient = "Bad Client"; WMI = "Broken WMI"; LowDisk = "Low Disk Space"; BackupError = "USMT Errors"; RestoreError = "USMT Errors"; Offline = "Can't Reach" }
|
||||
$script:DemoDeptTags = @{ Accounting = "Accounting"; Sales = "Sales Floor"; Executive = "Exec Suite" }
|
||||
|
||||
# Roster as an ordered table of tag -> @{ devices = List[string]; skipUSMT = bool }
|
||||
function ConvertTo-DemoRoster($tracked) {
|
||||
$r = [ordered]@{}
|
||||
foreach ($t in (Get-DemoTagList $tracked)) { $r[$t.Name] = @{ devices = [System.Collections.Generic.List[string]]::new([string[]]$t.Devices); skipUSMT = $t.Skip } }
|
||||
return $r
|
||||
}
|
||||
function Save-DemoRoster([string]$path, $roster) {
|
||||
# Date tags first, in date order, then the rest as they were
|
||||
$dated = @($roster.Keys | Where-Object { ConvertFrom-DemoDateTag $_ } | Sort-Object { ConvertFrom-DemoDateTag $_ })
|
||||
$out = [ordered]@{}
|
||||
foreach ($k in $dated + @($roster.Keys | Where-Object { $dated -notcontains $_ })) {
|
||||
if ($roster[$k].devices.Count -eq 0) { continue } # an empty tag is deleted
|
||||
$out[$k] = [ordered]@{ devices = @($roster[$k].devices); skipUSMT = [bool]$roster[$k].skipUSMT }
|
||||
}
|
||||
$tmp = "$path.tmp"
|
||||
[System.IO.File]::WriteAllText($tmp, ($out | ConvertTo-Json -Depth 5), [System.Text.Encoding]::UTF8)
|
||||
Move-Item -LiteralPath $tmp -Destination $path -Force
|
||||
}
|
||||
|
||||
function Get-DemoNewDevice($roster, $state) {
|
||||
$all = [System.Collections.Generic.HashSet[string]]::new()
|
||||
foreach ($t in $roster.Values) { foreach ($d in $t.devices) { foreach ($n in ($d -split '\s*,\s*')) { $null = $all.Add($n) } } }
|
||||
do {
|
||||
$state.Serial = [int]$state.Serial + 1
|
||||
$seed = "team$($state.Serial)"
|
||||
$r = Get-DemoRand $seed 'prefix'
|
||||
$prefix = if ($r -lt 0.55) { "PRVDT" } elseif ($r -lt 0.80) { "PUBDT" } elseif ($r -lt 0.95) { "PRVLT" } else { "PUBLT" }
|
||||
$name = "$prefix-$(New-DemoSerial $seed)"
|
||||
} while ($all.Contains($name))
|
||||
return $name
|
||||
}
|
||||
|
||||
function Remove-DemoFromRoster($roster, [string]$dev, [string[]]$only) {
|
||||
foreach ($k in @($roster.Keys)) { if (-not $only -or $only -contains $k) { $null = $roster[$k].devices.Remove($dev) } }
|
||||
}
|
||||
function Add-DemoToTag($roster, [string]$tag, [string]$dev) {
|
||||
if (-not $roster.Contains($tag)) { $roster[$tag] = @{ devices = [System.Collections.Generic.List[string]]::new(); skipUSMT = $false } }
|
||||
if (-not $roster[$tag].devices.Contains($dev)) { $roster[$tag].devices.Add($dev) }
|
||||
}
|
||||
function Get-DemoDateTagsOf($roster, [string]$dev) { return @($roster.Keys | Where-Object { (ConvertFrom-DemoDateTag $_) -and $roster[$_].devices.Contains($dev) }) }
|
||||
|
||||
# A stuck device: its problem has shown up by now
|
||||
function Get-DemoStuck([string]$dev) {
|
||||
$plan = Get-DemoPlan $dev
|
||||
if (-not $plan -or -not $plan.Fail -or (Get-Date) -lt $plan.FailAt) { return $null }
|
||||
return $plan.Fail
|
||||
}
|
||||
|
||||
function Move-DemoDevice($roster, $state, [string]$dev, [datetime]$to) {
|
||||
$from = Get-DemoDateTagsOf $roster $dev
|
||||
Remove-DemoFromRoster $roster $dev $from
|
||||
Add-DemoToTag $roster (ConvertTo-DemoDateTag $to) $dev
|
||||
return ($from -join ", ")
|
||||
}
|
||||
|
||||
# The days the team keeps tagged: the last few working days (still being cleaned up) and the
|
||||
# next two weeks of working days, today included
|
||||
function Get-DemoWindow {
|
||||
$today = (Get-Date).Date
|
||||
$kept = @(1..[int]$script:DemoTeamCfg.DaysKept | ForEach-Object { Get-DemoWorkday $today (-$_) })
|
||||
$ahead = @(0..([int]$script:DemoTeamCfg.DaysAhead - 1) | ForEach-Object { Get-DemoWorkday $today $_ })
|
||||
return @{ Oldest = ($kept | Sort-Object | Select-Object -First 1); Ahead = $ahead }
|
||||
}
|
||||
|
||||
function Invoke-DemoRollover($roster, $state, $log) {
|
||||
$w = Get-DemoWindow
|
||||
$tech = Get-Random -InputObject $script:DemoTechs
|
||||
foreach ($tag in @($roster.Keys)) {
|
||||
$day = ConvertFrom-DemoDateTag $tag
|
||||
if (-not $day -or $day -ge $w.Oldest) { continue }
|
||||
$done = 0; $moved = [System.Collections.Generic.List[string]]::new(); $retired = 0
|
||||
foreach ($dev in @($roster[$tag].devices)) {
|
||||
if ((Get-DemoStage (Get-DemoProfile $dev)).Name -eq 'Completed' -or -not (Get-DemoPlan $dev)) {
|
||||
Remove-DemoFromRoster $roster $dev; $done++; continue
|
||||
}
|
||||
$moves = [int]$state.Moves[$dev]
|
||||
if ($moves -ge 2) { Remove-DemoFromRoster $roster $dev; $retired++; continue }
|
||||
$state.Moves[$dev] = $moves + 1
|
||||
$to = $w.Ahead[[math]::Min($w.Ahead.Count - 1, (Get-Random -Minimum 1 -Maximum 4))]
|
||||
$null = Move-DemoDevice $roster $state $dev $to
|
||||
$moved.Add("$dev -> $(ConvertTo-DemoDateTag $to)")
|
||||
}
|
||||
$roster.Remove($tag)
|
||||
if ($done + $moved.Count + $retired -eq 0) { continue }
|
||||
$msg = "[Team] $tech closed out $tag`: $done completed device(s) untracked"
|
||||
if ($moved.Count) { $msg += ", rescheduled $($moved -join ', ')" }
|
||||
if ($retired) { $msg += ", $retired dropped after two reschedules" }
|
||||
$log.Add($msg)
|
||||
}
|
||||
# Fill any working day in the next two weeks that has nothing scheduled
|
||||
$per = [int]$script:DemoTeamCfg.DevicesPerDay
|
||||
foreach ($day in $w.Ahead) {
|
||||
$tag = ConvertTo-DemoDateTag $day
|
||||
if ($roster.Contains($tag) -and $roster[$tag].devices.Count) { continue }
|
||||
$n = [math]::Max(3, $per + (Get-Random -Minimum -3 -Maximum 4))
|
||||
$new = @(1..$n | ForEach-Object { Get-DemoNewDevice $roster $state })
|
||||
foreach ($dev in $new) {
|
||||
Add-DemoToTag $roster $tag $dev
|
||||
$dept = (Get-DemoProfile $dev).Department.Name
|
||||
if ($script:DemoDeptTags.ContainsKey($dept) -and (Get-Random -Maximum 1.0) -lt 0.6) { Add-DemoToTag $roster $script:DemoDeptTags[$dept] $dev }
|
||||
}
|
||||
$log.Add("[Team] $tech scheduled $n devices for $tag")
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DemoTeamAction($roster, $state, $log) {
|
||||
$w = Get-DemoWindow
|
||||
$today = (Get-Date).Date
|
||||
$tech = Get-Random -InputObject $script:DemoTechs
|
||||
$upcoming = @($w.Ahead | Where-Object { $_ -gt $today })
|
||||
$allDated = @($roster.Keys | Where-Object { ConvertFrom-DemoDateTag $_ })
|
||||
$devsOn = { param($days) @($allDated | Where-Object { (ConvertFrom-DemoDateTag $_) -in $days } | ForEach-Object { $roster[$_].devices } | Select-Object -Unique) }
|
||||
$issueNames = @($script:DemoIssueTags.Values | Select-Object -Unique)
|
||||
|
||||
$actions = @('label', 'label', 'fix', 'fix', 'add', 'add', 'move', 'move', 'drop', 'dept') | Sort-Object { Get-Random }
|
||||
foreach ($a in $actions) {
|
||||
switch ($a) {
|
||||
'label' {
|
||||
# Someone notices a stuck PC and tags it with its problem
|
||||
$cands = @(& $devsOn @($allDated | ForEach-Object { ConvertFrom-DemoDateTag $_ } | Where-Object { $_ -le $today }) | Where-Object {
|
||||
$k = Get-DemoStuck $_; $k -and -not ($roster.Contains($script:DemoIssueTags[$k]) -and $roster[$script:DemoIssueTags[$k]].devices.Contains($_)) })
|
||||
if (-not $cands.Count) { continue }
|
||||
$dev = Get-Random -InputObject $cands
|
||||
$tag = $script:DemoIssueTags[(Get-DemoStuck $dev)]
|
||||
Add-DemoToTag $roster $tag $dev
|
||||
$log.Add("[Team] $tech tagged $dev as '$tag'"); return
|
||||
}
|
||||
'fix' {
|
||||
# A problem gets fixed: the PC goes back on the schedule
|
||||
$cands = @($issueNames | Where-Object { $roster.Contains($_) } | ForEach-Object { $roster[$_].devices } | Select-Object -Unique)
|
||||
if (-not $cands.Count) { continue }
|
||||
$dev = Get-Random -InputObject $cands
|
||||
$kind = (Get-DemoPlan $dev).Fail
|
||||
$what = switch ($kind) {
|
||||
'BadClient' { "repaired the ConfigMgr client on" } 'WMI' { "rebuilt the WMI repository on" } 'LowDisk' { "cleared space on C: of" }
|
||||
'Offline' { "got the user to power on" } default { "cleared the USMT error on" }
|
||||
}
|
||||
$null = $state.Fixed.Add($dev)
|
||||
$script:DemoFixed.Add($dev) | Out-Null
|
||||
Remove-DemoFromRoster $roster $dev $issueNames
|
||||
$to = $w.Ahead[[math]::Min($w.Ahead.Count - 1, (Get-Random -Minimum 1 -Maximum 4))]
|
||||
$from = Move-DemoDevice $roster $state $dev $to
|
||||
$log.Add("[Team] $tech $what $dev; moved it from $from to $(ConvertTo-DemoDateTag $to)"); return
|
||||
}
|
||||
'add' {
|
||||
if (-not $upcoming.Count) { continue }
|
||||
$day = Get-Random -InputObject $upcoming
|
||||
$tag = ConvertTo-DemoDateTag $day
|
||||
$new = @(1..(Get-Random -Minimum 1 -Maximum 4) | ForEach-Object { Get-DemoNewDevice $roster $state })
|
||||
foreach ($dev in $new) { Add-DemoToTag $roster $tag $dev }
|
||||
$log.Add("[Team] $tech added $($new -join ', ') to $tag"); return
|
||||
}
|
||||
'move' {
|
||||
$cands = @(& $devsOn $upcoming | Where-Object { (Get-DemoStage (Get-DemoProfile $_)).Name -eq 'NotStarted' })
|
||||
if (-not $cands.Count -or $upcoming.Count -lt 2) { continue }
|
||||
$dev = Get-Random -InputObject $cands
|
||||
$cur = Get-DemoScheduleDate $dev
|
||||
$to = Get-Random -InputObject @($upcoming | Where-Object { $_ -ne $cur })
|
||||
$why = if ($to -lt $cur) { Get-Random -InputObject @("user asked to go earlier", "a slot opened up", "swapped with another desk") }
|
||||
else { Get-Random -InputObject @("user is out that day", "department asked for a later date", "waiting on a docking station", "swapped with another desk") }
|
||||
$from = Move-DemoDevice $roster $state $dev $to
|
||||
$log.Add("[Team] $tech moved $dev from $from to $(ConvertTo-DemoDateTag $to) ($why)"); return
|
||||
}
|
||||
'drop' {
|
||||
$cands = @(& $devsOn $upcoming | Where-Object { (Get-DemoStage (Get-DemoProfile $_)).Name -eq 'NotStarted' })
|
||||
if (-not $cands.Count) { continue }
|
||||
$dev = Get-Random -InputObject $cands
|
||||
$why = Get-Random -InputObject @("device is being retired", "already replaced last month", "user left the company", "duplicate entry")
|
||||
Remove-DemoFromRoster $roster $dev
|
||||
$log.Add("[Team] $tech removed $dev ($why)"); return
|
||||
}
|
||||
'dept' {
|
||||
$cands = @(& $devsOn $upcoming | Where-Object {
|
||||
$dep = (Get-DemoProfile $_).Department.Name
|
||||
$script:DemoDeptTags.ContainsKey($dep) -and -not ($roster.Contains($script:DemoDeptTags[$dep]) -and $roster[$script:DemoDeptTags[$dep]].devices.Contains($_)) })
|
||||
if (-not $cands.Count) { continue }
|
||||
$dev = Get-Random -InputObject $cands
|
||||
$tag = $script:DemoDeptTags[(Get-DemoProfile $dev).Department.Name]
|
||||
Add-DemoToTag $roster $tag $dev
|
||||
$log.Add("[Team] $tech added $dev to '$tag'"); return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-DemoScheduleDate([string]$dev) { Import-DemoSchedule; return $script:DemoSchedule[$dev] }
|
||||
|
||||
function Read-DemoTeamState([string]$path) {
|
||||
$state = @{ Day = ''; NextAction = (Get-Date).ToString("o"); Serial = 0; Fixed = [System.Collections.Generic.HashSet[string]]::new(); Moves = @{} }
|
||||
if (Test-Path $path) {
|
||||
try {
|
||||
$j = [System.IO.File]::ReadAllText($path) | ConvertFrom-Json
|
||||
$state.Day = "$($j.Day)"; $state.NextAction = "$($j.NextAction)"; $state.Serial = [int]$j.Serial
|
||||
foreach ($f in @($j.Fixed)) { if ($f) { $null = $state.Fixed.Add($f) } }
|
||||
if ($j.Moves) { foreach ($m in $j.Moves.PSObject.Properties) { $state.Moves[$m.Name] = [int]$m.Value } }
|
||||
} catch {}
|
||||
}
|
||||
return $state
|
||||
}
|
||||
function Save-DemoTeamState([string]$path, $state, $roster) {
|
||||
# Forget fixes and move counts for devices that are no longer tracked
|
||||
$live = [System.Collections.Generic.HashSet[string]]::new()
|
||||
foreach ($t in $roster.Values) { foreach ($d in $t.devices) { $null = $live.Add($d) } }
|
||||
$moves = [ordered]@{}
|
||||
foreach ($k in $state.Moves.Keys) { if ($live.Contains($k)) { $moves[$k] = $state.Moves[$k] } }
|
||||
$out = [ordered]@{
|
||||
Day = $state.Day; NextAction = $state.NextAction; Serial = $state.Serial
|
||||
Fixed = @($state.Fixed | Where-Object { $live.Contains($_) })
|
||||
Moves = $moves
|
||||
}
|
||||
[System.IO.File]::WriteAllText($path, ($out | ConvertTo-Json -Depth 4), [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
# Called by the worker at the start of every demo cycle
|
||||
function Invoke-DemoTeam([string]$DataDir) {
|
||||
$trackedPath = Join-Path $DataDir "Tracked-Devices.json"
|
||||
$statePath = Join-Path $DataDir "Demo-Team.json"
|
||||
if (-not (Test-Path $trackedPath)) { return }
|
||||
$state = Read-DemoTeamState $statePath
|
||||
try { $roster = ConvertTo-DemoRoster ([System.IO.File]::ReadAllText($trackedPath) | ConvertFrom-Json) } catch { return }
|
||||
Set-DemoSchedule $roster $state.Fixed -Pin
|
||||
|
||||
$log = [System.Collections.Generic.List[string]]::new()
|
||||
$now = Get-Date
|
||||
$todayKey = $now.ToString("yyyy-MM-dd")
|
||||
$dirty = -not (Test-Path $statePath)
|
||||
if ($state.Day -ne $todayKey) {
|
||||
Invoke-DemoRollover $roster $state $log
|
||||
$state.Day = $todayKey
|
||||
$dirty = $true
|
||||
}
|
||||
$ws = [timespan]$script:DemoTeamCfg.WorkStart; $we = [timespan]$script:DemoTeamCfg.WorkEnd
|
||||
$working = (Test-DemoWorkday $now) -and $now.TimeOfDay -ge $ws -and $now.TimeOfDay -lt $we
|
||||
$next = [datetime]::MinValue
|
||||
if (-not [datetime]::TryParse($state.NextAction, [ref]$next)) { $next = $now }
|
||||
if ($working -and $now -ge $next) {
|
||||
Invoke-DemoTeamAction $roster $state $log
|
||||
$range = @($script:DemoTeamCfg.ActionEveryMinutes)
|
||||
$state.NextAction = $now.AddMinutes((Get-Random -Minimum ([int]$range[0]) -Maximum ([int]$range[1] + 1))).ToString("o")
|
||||
$dirty = $true
|
||||
}
|
||||
$script:DemoSchedulePinned = $false
|
||||
$script:DemoScheduleChecked = [datetime]::MinValue # re-read the roster on the next lookup
|
||||
if (-not $dirty -and $log.Count -eq 0) { return }
|
||||
|
||||
if ($log.Count) { Save-DemoRoster $trackedPath $roster }
|
||||
Save-DemoTeamState $statePath $state $roster
|
||||
foreach ($line in $log) { Write-Host $line -ForegroundColor Magenta }
|
||||
}
|
||||
Reference in new issue
Block a user