mcp-pacemaker 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/CONTRIBUTING.md +46 -13
  3. package/README.md +109 -524
  4. package/THIRD_PARTY_NOTICES.txt +1 -28
  5. package/autostart/windows/account-task.ps1 +167 -0
  6. package/autostart/windows/inspect-task.ps1 +42 -0
  7. package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
  8. package/autostart/windows/legacy-access.ps1 +359 -0
  9. package/autostart/windows/legacy-task.ps1 +301 -0
  10. package/autostart/windows/register-task.ps1 +11 -2
  11. package/bin/account-task-adapter.mjs +47 -0
  12. package/bin/account-upgrade-controller.mjs +198 -0
  13. package/bin/account-upgrade-worker.mjs +141 -0
  14. package/bin/account-worker-session.mjs +215 -0
  15. package/bin/cli-dispatch.mjs +154 -0
  16. package/bin/cli-main.mjs +1063 -0
  17. package/bin/cli-selection.mjs +43 -0
  18. package/bin/cli.mjs +7 -876
  19. package/bin/dependency-lock.json +642 -0
  20. package/bin/legacy-1.3.json +32 -0
  21. package/bin/legacy-access.mjs +73 -0
  22. package/bin/legacy-installation.mjs +273 -0
  23. package/bin/legacy-task.mjs +76 -0
  24. package/bin/legacy-upgrade.mjs +527 -0
  25. package/bin/managed-runtime.mjs +170 -0
  26. package/bin/managed-state.mjs +257 -0
  27. package/bin/managed-upgrade.mjs +581 -0
  28. package/bin/mcp-bridge.mjs +150 -4
  29. package/bin/pooling-batches.mjs +13 -0
  30. package/bin/pooling-writer.mjs +4 -0
  31. package/bin/prewarm.mjs +59 -25
  32. package/bin/runtime-context.mjs +100 -0
  33. package/bin/service-control.mjs +32 -4
  34. package/bin/stable-launcher.mjs +54 -0
  35. package/bin/task-only-controller.mjs +226 -0
  36. package/bin/task-scope.mjs +65 -0
  37. package/bin/task-transaction-protocol.mjs +173 -0
  38. package/bin/top.mjs +22 -10
  39. package/bin/update-check.mjs +13 -2
  40. package/bin/upgrade-autostart.mjs +47 -0
  41. package/bin/upgrade-capability.json +9 -0
  42. package/bin/upgrade-dependencies.mjs +183 -0
  43. package/bin/upgrade-package.mjs +205 -0
  44. package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
  45. package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
  46. package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
  47. package/bin/windows-legacy/src/LegacyNative.cs +832 -0
  48. package/bin/windows-legacy/src/LegacyProcessBroker.cs +961 -0
  49. package/bin/windows-legacy-process.mjs +248 -0
  50. package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
  51. package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
  52. package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
  53. package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
  54. package/bin/windows-process-lifetime.mjs +218 -0
  55. package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
  56. package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
  57. package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
  58. package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
  59. package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
  60. package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
  61. package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
  62. package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
  63. package/bin/windows-task-channel.mjs +335 -0
  64. package/docs/account-upgrades.md +218 -0
  65. package/docs/compatibility.md +39 -0
  66. package/docs/configuration.md +146 -0
  67. package/docs/dashboards.md +125 -0
  68. package/docs/execution-context.md +119 -0
  69. package/docs/http-api.md +47 -0
  70. package/docs/install.md +52 -0
  71. package/docs/operations.md +99 -0
  72. package/docs/upgrades.md +324 -0
  73. package/package.json +19 -5
  74. package/supervisor/supervise.mjs +226 -40
  75. package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
  76. package/tools/windows-legacy/README.md +251 -0
  77. package/tools/windows-legacy/build.ps1 +122 -0
  78. package/tools/windows-legacy/inventory.mjs +92 -0
  79. package/tools/windows-process-lifetime/README.md +119 -0
  80. package/tools/windows-process-lifetime/build.ps1 +122 -0
  81. package/tools/windows-process-lifetime/inventory.mjs +86 -0
  82. package/tools/windows-task-channel/README.md +329 -0
  83. package/tools/windows-task-channel/build.ps1 +107 -0
  84. package/tools/windows-task-channel/inventory.mjs +117 -0
  85. package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
  86. package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
  87. package/ui/dist/assets/index-PVcuDNrN.css +1 -0
  88. package/ui/dist/index.html +2 -2
  89. package/ui/dist/third-party-manifest.json +37 -25
  90. package/ui/dist/assets/index-CaLPg0c-.css +0 -1
@@ -192,7 +192,7 @@ SOFTWARE.
192
192
  */
193
193
 
194
194
  ==============================================================================
195
- tailwindcss@3.4.19
195
+ tailwindcss@4.3.3
196
196
  Declared license: MIT
197
197
 
198
198
  --- LICENSE ---
@@ -218,33 +218,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
218
218
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
219
219
  SOFTWARE.
220
220
 
221
- --- lib/css/LICENSE ---
222
- MIT License
223
-
224
- Copyright (c) Nicolas Gallagher
225
- Copyright (c) Jonathan Neal
226
- Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
227
- Copyright (c) Adam Wathan
228
- Copyright (c) Jonathan Reinink
229
-
230
- Permission is hereby granted, free of charge, to any person obtaining a copy
231
- of this software and associated documentation files (the "Software"), to deal
232
- in the Software without restriction, including without limitation the rights
233
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
234
- copies of the Software, and to permit persons to whom the Software is
235
- furnished to do so, subject to the following conditions:
236
-
237
- The above copyright notice and this permission notice shall be included in all
238
- copies or substantial portions of the Software.
239
-
240
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
241
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
242
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
243
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
244
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
245
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
246
- SOFTWARE.
247
-
248
221
  ==============================================================================
249
222
  vite@7.3.6
250
223
  Declared license: MIT
@@ -0,0 +1,167 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Performs fixed controller task operations while preserving the registered account.
4
+ .DESCRIPTION
5
+ Reads a bounded controller request on stdin. This adapter changes only the exact
6
+ selected registration, preserving principal, logon type, run level and full security.
7
+ It never stops processes, edits configuration, enables privileges or changes ACLs.
8
+ .PARAMETER Action
9
+ Exact task operation chosen by the trusted controller.
10
+ .OUTPUTS
11
+ Bounded JSON task/profile/session evidence. A successful launch call is not readiness.
12
+ .EXAMPLE
13
+ .\account-task.ps1 -Action inspect
14
+ #>
15
+ [CmdletBinding()]
16
+ param(
17
+ [Parameter(Mandatory)]
18
+ [ValidateSet('inspect', 'session', 'bootstrap', 'hold', 'repoint', 'release', 'restore', 'launch')]
19
+ [string]$Action
20
+ )
21
+ $ErrorActionPreference = 'Stop'
22
+ . (Join-Path $PSScriptRoot 'legacy-task.ps1') -Operation inspect -Port 1 -FunctionsOnly
23
+ $text = [Console]::In.ReadToEnd()
24
+ if ($text.Length -gt 65536) { throw 'Controller request exceeds its bound.' }
25
+ $request = $text | ConvertFrom-Json
26
+ $targetSid = $request.targetSid
27
+ if ($targetSid -cnotmatch '^S-1-[0-9]+(?:-[0-9]+)+$' -or
28
+ ([Security.Principal.SecurityIdentifier]::new($targetSid)).Value -cne $targetSid) {
29
+ throw 'A canonical target SID is required.'
30
+ }
31
+ $impersonated = [Security.Principal.WindowsIdentity]::GetCurrent($true)
32
+ if ($null -ne $impersonated) {
33
+ $impersonated.Dispose()
34
+ throw 'Impersonating task-adapter contexts are unsupported.'
35
+ }
36
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
37
+ $principal = [Security.Principal.WindowsPrincipal]::new($identity)
38
+ if ($targetSid -cne $currentSid -and
39
+ -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
40
+ throw 'The current task-adapter token is not an enabled administrator.'
41
+ }
42
+ $path = $request.taskPath
43
+ if ([string]::IsNullOrEmpty($path) -or
44
+ $path.Length -gt 1024 -or
45
+ -not $path.StartsWith('\') -or
46
+ $path.Contains('/') -or
47
+ $path.Split('\') -contains '..') { throw 'An exact task path is required.' }
48
+ $separator = $path.LastIndexOf('\')
49
+ $folderPath = if ($separator -eq 0) { '\' } else { $path.Substring(0, $separator) }
50
+ $name = $path.Substring($separator + 1)
51
+ $service = New-Object -ComObject Schedule.Service
52
+ $service.Connect()
53
+ $folder = $service.GetFolder($folderPath)
54
+ $task = $folder.GetTask($name)
55
+ $record = Get-TaskRecord -Task $task
56
+ $record.runAsName = $task.Definition.Principal.UserId
57
+ if ($record.path -cne $path -or
58
+ $record.userSid -cne $targetSid -or
59
+ $record.logonType -ne 3 -or
60
+ $record.runLevel -ne 0) { throw 'Selected task does not preserve the requested interactive account.' }
61
+ Assert-TaskSecurity -Actual $record.security -Expected $record.security -RuntimeSid $targetSid
62
+ if ($Action -eq 'inspect') {
63
+ $profiles = @(Get-CimInstance -ClassName Win32_UserProfile -Filter "SID='$targetSid'" -Property SID, LocalPath, Loaded -OperationTimeoutSec 10)
64
+ if ($profiles.Count -ne 1 -or
65
+ -not $profiles[0].Loaded) { throw 'The selected user profile is not loaded in an existing interactive session.' }
66
+ @{
67
+ record = $record
68
+ profilePath = [IO.Path]::GetFullPath($profiles[0].LocalPath)
69
+ allowDemandStart = $task.Definition.Settings.AllowDemandStart
70
+ } | ConvertTo-Json -Depth 12 -Compress
71
+ return
72
+ }
73
+ if ($record.xmlSha256 -cne $request.expected.xmlSha256 -or
74
+ $record.userSid -cne $request.expected.userSid) { throw 'Task definition changed after the approved plan.' }
75
+ Assert-TaskSecurity -Actual $record.security -Expected $request.expected.security -RuntimeSid $targetSid
76
+ if ($Action -eq 'session') {
77
+ $port = [int]$request.port
78
+ if ($port -lt 1 -or $port -gt 65535) { throw 'Invalid selected port.' }
79
+ $listeners = @(Get-CimInstance -Namespace 'root/StandardCimv2' -ClassName MSFT_NetTCPConnection `
80
+ -Filter "LocalAddress='127.0.0.1' AND LocalPort=$port AND State=2" -OperationTimeoutSec 10)
81
+ if ($listeners.Count -ne 1) { throw 'Selected listener is absent or ambiguous.' }
82
+ $processId = [int]$listeners[0].OwningProcess
83
+ $process = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$processId" -Property ProcessId, ParentProcessId, SessionId, CreationDate, CommandLine, ExecutablePath -OperationTimeoutSec 10
84
+ $owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -ErrorAction Stop
85
+ $scriptPath = [IO.Path]::GetFullPath((Join-Path $request.root 'bin\mcp-bridge.mjs'))
86
+ $commands = @(
87
+ "`"$($process.ExecutablePath)`" `"$scriptPath`" --port $port",
88
+ "$($process.ExecutablePath) `"$scriptPath`" --port $port"
89
+ )
90
+ if ($null -ne $request.managed) {
91
+ $parts = @((Join-Path $request.root 'supervisor\bridge-child.mjs'), '--port', "$port", '--config', $request.managed.config, '--cwd', $request.managed.cwd)
92
+ $arguments = ($parts | ForEach-Object {
93
+ if ($_ -match '\s') { "`"$_`"" } else { $_ }
94
+ }) -join ' '
95
+ $commands = @("`"$($process.ExecutablePath)`" $arguments", "$($process.ExecutablePath) $arguments")
96
+ }
97
+ if ($owner.ReturnValue -ne 0 -or
98
+ $owner.Sid -cne $targetSid -or
99
+ $process.SessionId -le 0 -or
100
+ $process.CommandLine -notin $commands) {
101
+ throw 'Selected listener account/session/absolute legacy command could not be verified.'
102
+ }
103
+ $parentId = [int]$process.ParentProcessId
104
+ $parent = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$parentId" -Property ProcessId, ExecutablePath, SessionId, CreationDate -OperationTimeoutSec 10
105
+ $parentOwner = Invoke-CimMethod -InputObject $parent -MethodName GetOwnerSid -ErrorAction Stop
106
+ if ($parentOwner.ReturnValue -ne 0 -or
107
+ $parentOwner.Sid -cne $targetSid -or
108
+ $parent.SessionId -ne $process.SessionId -or
109
+ $parent.CreationDate -gt $process.CreationDate) { throw 'Selected supervisor account/session/generation is ambiguous.' }
110
+ @{ ownerSid = $owner.Sid; sessionId = $process.SessionId; pid = $processId
111
+ createdUtc = $process.CreationDate.ToUniversalTime().ToString('o')
112
+ supervisorImage = @{ path = $parent.ExecutablePath; sha256 = (Get-FileHash -LiteralPath $parent.ExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant() }
113
+ } | ConvertTo-Json -Depth 4 -Compress
114
+ return
115
+ }
116
+ if ($Action -eq 'launch') {
117
+ if (-not $task.Enabled -or
118
+ -not $task.Definition.Settings.AllowDemandStart -or
119
+ $request.sessionId -le 0) { throw 'The selected task cannot demand-start in the requested session.' }
120
+ $running = $task.RunEx($null, 12, [int]$request.sessionId, $targetSid)
121
+ @{ requested = $true; instanceGuid = $running.InstanceGuid; readinessProven = $false } | ConvertTo-Json -Compress
122
+ return
123
+ }
124
+ $definition = $task.Definition
125
+ switch ($Action) {
126
+ 'bootstrap' {
127
+ if (-not $task.Enabled -or
128
+ -not $definition.Settings.AllowDemandStart) { throw 'Bootstrap requires an enabled demand-start task.' }
129
+ if ($definition.Actions.Count -ne 1) { throw 'Unsupported task actions.' }
130
+ $definition.Actions.Item(1).Path = $request.bootstrap.nodePath
131
+ $definition.Actions.Item(1).Arguments = $request.bootstrap.arguments
132
+ $definition.Actions.Item(1).WorkingDirectory = $request.bootstrap.cwd
133
+ foreach ($trigger in $definition.Triggers) { $trigger.Enabled = $false }
134
+ $definition.Settings.RestartCount = 0
135
+ }
136
+ 'repoint' {
137
+ if ($definition.Actions.Count -ne 1) { throw 'Unsupported task actions.' }
138
+ $definition.Actions.Item(1).Path = 'wscript.exe'
139
+ $definition.Actions.Item(1).Arguments = "`"$($request.launcher)`""
140
+ $definition.Actions.Item(1).WorkingDirectory = ''
141
+ }
142
+ 'hold' {
143
+ foreach ($trigger in $definition.Triggers) { $trigger.Enabled = $false }
144
+ $definition.Settings.RestartCount = 0
145
+ }
146
+ 'release' {
147
+ $original = $service.NewTask(0)
148
+ $original.XmlText = $request.original.xml
149
+ if ($original.Triggers.Count -ne $definition.Triggers.Count) { throw 'Task trigger identity changed.' }
150
+ for ($index = 1; $index -le $definition.Triggers.Count; $index++) {
151
+ $definition.Triggers.Item($index).Enabled = $original.Triggers.Item($index).Enabled
152
+ }
153
+ $definition.Settings.RestartCount = $original.Settings.RestartCount
154
+ }
155
+ 'restore' { $definition.XmlText = $request.original.xml }
156
+ }
157
+ Assert-TaskSecurity -Actual $record.security -Expected $request.original.security -RuntimeSid $targetSid
158
+ if ((Get-UserSid -User $definition.Principal.UserId) -cne $targetSid -or
159
+ $definition.Principal.LogonType -ne $record.logonType -or
160
+ $definition.Principal.RunLevel -ne $record.runLevel) { throw 'Task account semantics changed.' }
161
+ $expectedXml = $definition.XmlText
162
+ $folder.RegisterTaskDefinition($name, $definition, 52, $definition.Principal.UserId, $null, 3, $record.security.sddl) | Out-Null
163
+ $updated = Get-TaskRecord -Task ($folder.GetTask($name))
164
+ $updated.runAsName = $definition.Principal.UserId
165
+ Assert-TaskSecurity -Actual $updated.security -Expected $record.security -RuntimeSid $targetSid
166
+ if ($updated.xmlSha256 -cne (Get-XmlHash -Xml $expectedXml)) { throw 'Task update outcome differs from the exact requested definition.' }
167
+ $updated | ConvertTo-Json -Depth 12 -Compress
@@ -0,0 +1,42 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Reads the selected per-user Pacemaker task and its legacy task identity.
4
+ .DESCRIPTION
5
+ Returns task actions and principal data without changing tasks or processes.
6
+ .PARAMETER Port
7
+ Selected service port.
8
+ .OUTPUTS
9
+ JSON array of existing task identities.
10
+ .EXAMPLE
11
+ .\inspect-task.ps1 -Port 8791
12
+ #>
13
+ [CmdletBinding()]
14
+ param([ValidateRange(1, 65535)][int]$Port)
15
+
16
+ $ErrorActionPreference = 'Stop'
17
+ $service = New-Object -ComObject Schedule.Service
18
+ $service.Connect()
19
+ $folder = $service.GetFolder('\')
20
+ $names = @("McpPacemaker-$Port", 'McpPacemaker')
21
+ $records = @()
22
+ foreach ($task in $folder.GetTasks(1)) {
23
+ if ($task.Name -notin $names) { continue }
24
+ $actions = @()
25
+ foreach ($action in $task.Definition.Actions) {
26
+ $actions += @{
27
+ type = $action.Type
28
+ path = $action.Path
29
+ arguments = $action.Arguments
30
+ workingDirectory = $action.WorkingDirectory
31
+ }
32
+ }
33
+ $records += @{
34
+ name = $task.Name
35
+ enabled = $task.Enabled
36
+ user = $task.Definition.Principal.UserId
37
+ logonType = $task.Definition.Principal.LogonType
38
+ runLevel = $task.Definition.Principal.RunLevel
39
+ actions = $actions
40
+ }
41
+ }
42
+ ConvertTo-Json -InputObject $records -Depth 5 -Compress
@@ -0,0 +1,155 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Checks session facts for the original-user broker's bounded captured identities.
4
+ .DESCRIPTION
5
+ Reads a same-user request on stdin. Queries only its listed process identities,
6
+ or the selected managed loopback listener, and performs no process or security
7
+ mutation. Native held-generation revalidation remains the stop authority.
8
+ .OUTPUTS
9
+ A verification result without command lines or account identifiers.
10
+ .EXAMPLE
11
+ .\inspect-worker-process-sessions.ps1
12
+ #>
13
+ [CmdletBinding()]
14
+ param()
15
+ $ErrorActionPreference = 'Stop'
16
+ $watch = [Diagnostics.Stopwatch]::StartNew()
17
+ $stream = [Console]::OpenStandardInput()
18
+ $bytes = [byte[]]::new(131073)
19
+ $length = 0
20
+ while ($length -lt $bytes.Length) {
21
+ $read = $stream.Read($bytes, $length, $bytes.Length - $length)
22
+ if ($read -eq 0) { break }
23
+ $length += $read
24
+ }
25
+ if ($length -gt 131072) { throw 'Worker identity request exceeds its bound.' }
26
+ $text = [Text.UTF8Encoding]::new($false, $true).GetString($bytes, 0, $length)
27
+ $request = $text | ConvertFrom-Json
28
+ $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
29
+ $hasSessionType = $request.sessionId -is [long] -or $request.sessionId -is [int]
30
+ if ($request.ownerSid -isnot [string] -or
31
+ $request.ownerSid -cne $sid -or
32
+ $request.ownerSid.Length -gt 184 -or
33
+ -not $hasSessionType -or
34
+ $request.sessionId -ne [Diagnostics.Process]::GetCurrentProcess().SessionId) {
35
+ throw 'Worker process/session scope is not the actual current account.'
36
+ }
37
+ if ($null -ne $request.port) {
38
+ if ((($request.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'ownerSid,port,sessionId') {
39
+ throw 'Invalid managed listener request shape.'
40
+ }
41
+ $port = [int]$request.port
42
+ if ($port -lt 1 -or
43
+ $port -gt 65535 -or
44
+ $null -ne $request.identities) { throw 'Invalid managed listener scope.' }
45
+ $listeners = @(Get-CimInstance -Namespace 'root/StandardCimv2' -ClassName MSFT_NetTCPConnection `
46
+ -Filter "LocalAddress='127.0.0.1' AND LocalPort=$port AND State=2" -OperationTimeoutSec 10)
47
+ # A verified stopped/failed-start generation has no listener; its native proof is checked by the caller.
48
+ if ($listeners.Count -eq 0) {
49
+ '{"verified":true}'
50
+ return
51
+ }
52
+ if ($listeners.Count -ne 1) { throw 'Managed listener is ambiguous.' }
53
+ $processId = [int]$listeners[0].OwningProcess
54
+ $row = Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$processId" -OperationTimeoutSec 10
55
+ $owner = Invoke-CimMethod -InputObject $row -MethodName GetOwnerSid -ErrorAction Stop
56
+ if ($null -eq $row -or
57
+ $row.SessionId -ne $request.sessionId -or
58
+ $owner.ReturnValue -ne 0 -or
59
+ $owner.Sid -cne $sid) { throw 'Managed listener account/session changed; replan before stop.' }
60
+ '{"verified":true}'
61
+ return
62
+ }
63
+ if ((($request.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'identities,ownerSid,sessionId' -or
64
+ $request.identities -isnot [Array]) { throw 'Invalid captured identity request shape.' }
65
+ $identities = @($request.identities)
66
+ if ($identities.Count -lt 2 -or
67
+ $identities.Count -gt 512) { throw 'Captured identity count is unsupported.' }
68
+ $seen = @{}
69
+ foreach ($identity in $identities) {
70
+ if ((($identity.PSObject.Properties.Name | Sort-Object) -join ',') -cne 'creationTime,ownerSid,pid' -or
71
+ ($identity.pid -isnot [long] -and $identity.pid -isnot [int]) -or
72
+ $identity.pid -le 0 -or
73
+ $identity.pid -gt [int]::MaxValue -or
74
+ $identity.ownerSid -isnot [string] -or
75
+ $identity.creationTime -isnot [string]) { throw 'Invalid captured identity shape.' }
76
+ $number = [int]$identity.pid
77
+ if ($seen.ContainsKey($number) -or
78
+ $identity.ownerSid -cne $sid -or
79
+ $identity.creationTime -cnotmatch '^[1-9][0-9]{0,18}$') {
80
+ throw 'Invalid original-user captured identity.'
81
+ }
82
+ $null = [DateTime]::FromFileTimeUtc([long]$identity.creationTime)
83
+ $seen[$number] = $true
84
+ }
85
+ $query = {
86
+ param([string]$Filter)
87
+ $ErrorActionPreference = 'Stop'
88
+ Import-Module CimCmdlets -ErrorAction Stop
89
+ $session = New-CimSession -ErrorAction Stop
90
+ try {
91
+ foreach ($row in Get-CimInstance -CimSession $session -ClassName Win32_Process -Filter $Filter `
92
+ -Property ProcessId, SessionId, CreationDate -OperationTimeoutSec 2) {
93
+ [pscustomobject]@{
94
+ pid = [int]$row.ProcessId
95
+ sessionId = [int]$row.SessionId
96
+ creationTicks = $row.CreationDate.ToUniversalTime().Ticks
97
+ }
98
+ }
99
+ } finally { Remove-CimSession -CimSession $session }
100
+ }
101
+ $observed = @{}
102
+ $pool = [RunspaceFactory]::CreateRunspacePool(1, 4)
103
+ $pool.Open()
104
+ try {
105
+ for ($offset = 0; $offset -lt $identities.Count; $offset += 128) {
106
+ $pending = @()
107
+ try {
108
+ for ($index = $offset; $index -lt [Math]::Min($offset + 128, $identities.Count); $index += 32) {
109
+ if ($watch.ElapsedMilliseconds -ge 13000) { throw 'Worker session query deadline.' }
110
+ $batch = @($identities[$index..([Math]::Min($index + 31, $identities.Count - 1))])
111
+ $expected = @{}
112
+ $clauses = foreach ($identity in $batch) {
113
+ $expected[[int]$identity.pid] = $true
114
+ 'ProcessId=' + ([int]$identity.pid).ToString([Globalization.CultureInfo]::InvariantCulture)
115
+ }
116
+ $pipeline = [PowerShell]::Create()
117
+ $pipeline.RunspacePool = $pool
118
+ $null = $pipeline.AddScript($query.ToString()).AddArgument(($clauses -join ' OR '))
119
+ $pending += @{ Pipeline = $pipeline; Expected = $expected; Result = $pipeline.BeginInvoke() }
120
+ }
121
+ foreach ($item in $pending) {
122
+ while (-not $item.Result.IsCompleted) {
123
+ if ($watch.ElapsedMilliseconds -ge 13000) { throw 'Worker session query deadline.' }
124
+ Start-Sleep -Milliseconds 10
125
+ }
126
+ $rows = @($item.Pipeline.EndInvoke($item.Result))
127
+ if ($item.Pipeline.HadErrors -or
128
+ $rows.Count -ne $item.Expected.Count) { throw 'Worker session query did not return its exact captured set.' }
129
+ foreach ($row in $rows) {
130
+ $number = [int]$row.pid
131
+ if (-not $item.Expected.ContainsKey($number) -or
132
+ $observed.ContainsKey($number)) { throw 'Worker session query returned an unexpected or duplicate identity.' }
133
+ $observed[$number] = $row
134
+ }
135
+ }
136
+ } finally {
137
+ foreach ($item in $pending) {
138
+ if (-not $item.Result.IsCompleted) { $item.Pipeline.Stop() }
139
+ $item.Pipeline.Dispose()
140
+ }
141
+ }
142
+ }
143
+ } finally { $pool.Close(); $pool.Dispose() }
144
+ foreach ($identity in $identities) {
145
+ $matching = $observed[[int]$identity.pid]
146
+ if ($null -eq $matching -or
147
+ $matching.sessionId -ne $request.sessionId) { throw 'Captured generation/session changed; replan before stop.' }
148
+ $actual = $matching.creationTicks
149
+ $expected = [DateTime]::FromFileTimeUtc([long]$identity.creationTime).Ticks
150
+ # CIM creation dates have microsecond precision; this read never authorizes termination.
151
+ if ([decimal]::Truncate([decimal]$actual / 10) -ne [decimal]::Truncate([decimal]$expected / 10)) {
152
+ throw 'Captured process birth changed; replan before stop.'
153
+ }
154
+ }
155
+ '{"verified":true}'