gentle-pi 2.7.0 → 3.0.0
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.
- package/README.md +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
# PS5.1 helper-owned Windows presence metadata boundary. It intentionally has
|
|
2
|
+
# no pipe server: start remains partial and publication never advertises a
|
|
3
|
+
# fabricated active listener.
|
|
4
|
+
# Native definitions adapted from windows-native-boundary-clean/tests/windows-native-boundary/native.ps1
|
|
5
|
+
# at c59e1598 (NtCreateFile rooted opens, GetSecurityInfo, and ABI layout).
|
|
6
|
+
# API provenance: NtCreateFile / OBJECT_ATTRIBUTES / NtQueryDirectoryFile are
|
|
7
|
+
# documented by Microsoft Win32/WDK; this helper has no external binary dependency.
|
|
8
|
+
[Console]::Out.WriteLine('{"event":"startup-marker","marker":"script-entered"}')
|
|
9
|
+
Set-StrictMode -Version Latest
|
|
10
|
+
$ErrorActionPreference = 'Stop'
|
|
11
|
+
$maxControlBytes = 16384
|
|
12
|
+
$maxBootstrapDiagnosticText = 4096
|
|
13
|
+
$nativeReady = $false
|
|
14
|
+
|
|
15
|
+
function Add-BootstrapDiagnosticText([System.Text.StringBuilder]$builder, [object]$value) {
|
|
16
|
+
if ($null -eq $value -or $builder.Length -ge $maxBootstrapDiagnosticText) { return }
|
|
17
|
+
$text = [string]$value
|
|
18
|
+
$remaining = $maxBootstrapDiagnosticText - $builder.Length
|
|
19
|
+
if ($text.Length -gt $remaining) { $text = $text.Substring(0, $remaining) }
|
|
20
|
+
[void]$builder.Append($text)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Get-BootstrapLanguageMode {
|
|
24
|
+
try {
|
|
25
|
+
switch ([string]$ExecutionContext.SessionState.LanguageMode) {
|
|
26
|
+
'FullLanguage' { return 'full' }
|
|
27
|
+
'ConstrainedLanguage' { return 'constrained' }
|
|
28
|
+
'RestrictedLanguage' { return 'restricted' }
|
|
29
|
+
'NoLanguage' { return 'no-language' }
|
|
30
|
+
default { return 'unknown' }
|
|
31
|
+
}
|
|
32
|
+
} catch { return 'unknown' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Get-BootstrapProperty([object]$value, [string]$name) {
|
|
36
|
+
try {
|
|
37
|
+
if ($null -eq $value) { return $null }
|
|
38
|
+
$property = $value.PSObject.Properties[$name]
|
|
39
|
+
if ($null -eq $property) { return $null }
|
|
40
|
+
return $property.Value
|
|
41
|
+
} catch { return $null }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ConvertTo-BootstrapDiagnosticRecord([object]$entry) {
|
|
45
|
+
$candidate = $entry
|
|
46
|
+
for ($depth = 0; $depth -lt 8 -and $null -ne $candidate; $depth++) {
|
|
47
|
+
if ($candidate -is [System.Management.Automation.ErrorRecord]) {
|
|
48
|
+
$fqid = Get-BootstrapProperty $candidate 'FullyQualifiedErrorId'
|
|
49
|
+
$categoryInfo = Get-BootstrapProperty $candidate 'CategoryInfo'
|
|
50
|
+
$category = Get-BootstrapProperty $categoryInfo 'Category'
|
|
51
|
+
$errorDetails = Get-BootstrapProperty $candidate 'ErrorDetails'
|
|
52
|
+
$errorDetailsMessage = Get-BootstrapProperty $errorDetails 'Message'
|
|
53
|
+
$exception = Get-BootstrapProperty $candidate 'Exception'
|
|
54
|
+
$targetObject = Get-BootstrapProperty $candidate 'TargetObject'
|
|
55
|
+
return [pscustomobject]@{
|
|
56
|
+
FullyQualifiedErrorId = if ($fqid -is [string]) { $fqid } else { $null }
|
|
57
|
+
Category = if ($category -is [System.Management.Automation.ErrorCategory]) { $category } else { $null }
|
|
58
|
+
ErrorDetailsMessage = if ($errorDetailsMessage -is [string]) { $errorDetailsMessage } else { $null }
|
|
59
|
+
Exception = if ($exception -is [System.Exception]) { $exception } else { $null }
|
|
60
|
+
TargetObject = if ($targetObject -is [System.CodeDom.Compiler.CompilerError]) { $targetObject } else { $null }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
$wrapped = Get-BootstrapProperty $candidate 'ErrorRecord'
|
|
64
|
+
if ($wrapped -is [System.Management.Automation.ErrorRecord]) {
|
|
65
|
+
$candidate = $wrapped
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
if ($candidate -is [System.Exception]) {
|
|
69
|
+
return [pscustomobject]@{ FullyQualifiedErrorId = $null; Category = $null; ErrorDetailsMessage = $null; Exception = $candidate }
|
|
70
|
+
}
|
|
71
|
+
$candidate = $wrapped
|
|
72
|
+
}
|
|
73
|
+
return $null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Get-BootstrapCompilerErrorCode([object]$target) {
|
|
77
|
+
try {
|
|
78
|
+
if ($target -isnot [System.CodeDom.Compiler.CompilerError]) { return $null }
|
|
79
|
+
$errorNumber = Get-BootstrapProperty $target 'ErrorNumber'
|
|
80
|
+
if ($errorNumber -is [string] -and $errorNumber -cmatch '\ACS[0-9]{4}\z') { return $errorNumber }
|
|
81
|
+
} catch {}
|
|
82
|
+
return $null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Get-BootstrapAddTypeReason([object]$record) {
|
|
86
|
+
$fqid = Get-BootstrapProperty $record 'FullyQualifiedErrorId'
|
|
87
|
+
if ($fqid -isnot [string]) { return 'unknown' }
|
|
88
|
+
$id = [regex]::Match($fqid, '^[^,]+').Value
|
|
89
|
+
switch ($id) {
|
|
90
|
+
'SOURCE_CODE_ERROR' { return 'source-code-error' }
|
|
91
|
+
'TYPE_ALREADY_EXISTS' { return 'type-already-exists' }
|
|
92
|
+
default { return 'unknown' }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Get-BootstrapDiagnosticCategory([object]$record) {
|
|
97
|
+
$recordCategory = Get-BootstrapProperty $record 'Category'
|
|
98
|
+
switch ($recordCategory) {
|
|
99
|
+
([System.Management.Automation.ErrorCategory]::InvalidArgument) { return 'argument' }
|
|
100
|
+
([System.Management.Automation.ErrorCategory]::InvalidOperation) { return 'invalid-operation' }
|
|
101
|
+
([System.Management.Automation.ErrorCategory]::NotImplemented) { return 'not-supported' }
|
|
102
|
+
([System.Management.Automation.ErrorCategory]::SecurityError) { return 'security' }
|
|
103
|
+
}
|
|
104
|
+
$exception = Get-BootstrapProperty $record 'Exception'
|
|
105
|
+
for ($depth = 0; $depth -lt 8 -and $exception -is [System.Exception]; $depth++) {
|
|
106
|
+
if ($exception -is [System.ArgumentException]) { return 'argument' }
|
|
107
|
+
if ($exception -is [System.InvalidOperationException]) { return 'invalid-operation' }
|
|
108
|
+
if ($exception -is [System.NotSupportedException]) { return 'not-supported' }
|
|
109
|
+
if ($exception -is [System.Security.SecurityException] -or $exception -is [System.UnauthorizedAccessException]) { return 'security' }
|
|
110
|
+
if ($exception -is [System.IO.FileLoadException] -or $exception -is [System.IO.FileNotFoundException] -or $exception -is [System.BadImageFormatException]) { return 'assembly-load' }
|
|
111
|
+
if ($exception -is [System.TypeLoadException] -or $exception -is [System.Reflection.ReflectionTypeLoadException]) { return 'type-load' }
|
|
112
|
+
$next = Get-BootstrapProperty $exception 'InnerException'
|
|
113
|
+
$exception = if ($next -is [System.Exception]) { $next } else { $null }
|
|
114
|
+
}
|
|
115
|
+
return 'other'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Write-BootstrapDiagnosticFallback {
|
|
119
|
+
try {
|
|
120
|
+
[Console]::Error.WriteLine(([pscustomobject]@{ kind = 'windows-session-bootstrap-diagnostic'; category = 'other'; compilerCodes = @(); reason = 'unknown'; languageMode = 'unknown' } | ConvertTo-Json -Compress))
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Write-BootstrapDiagnostic([object[]]$records) {
|
|
125
|
+
try {
|
|
126
|
+
$category = 'other'
|
|
127
|
+
$reason = 'unknown'
|
|
128
|
+
$compilerCodes = [System.Collections.Generic.List[string]]::new()
|
|
129
|
+
$seenCompilerCodes = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
130
|
+
$text = [System.Text.StringBuilder]::new()
|
|
131
|
+
$captured = @()
|
|
132
|
+
foreach ($entry in $records) {
|
|
133
|
+
if ($captured.Count -ge 8) { break }
|
|
134
|
+
$record = ConvertTo-BootstrapDiagnosticRecord $entry
|
|
135
|
+
if ($null -ne $record) { $captured += $record }
|
|
136
|
+
}
|
|
137
|
+
foreach ($record in $captured) {
|
|
138
|
+
if ($category -eq 'other') { $category = Get-BootstrapDiagnosticCategory $record }
|
|
139
|
+
if ($reason -eq 'unknown') { $reason = Get-BootstrapAddTypeReason $record }
|
|
140
|
+
Add-BootstrapDiagnosticText $text (Get-BootstrapProperty $record 'FullyQualifiedErrorId')
|
|
141
|
+
Add-BootstrapDiagnosticText $text (Get-BootstrapProperty $record 'ErrorDetailsMessage')
|
|
142
|
+
}
|
|
143
|
+
foreach ($record in $captured) {
|
|
144
|
+
$code = Get-BootstrapCompilerErrorCode (Get-BootstrapProperty $record 'TargetObject')
|
|
145
|
+
if ($code -is [string] -and $seenCompilerCodes.Add($code) -and $compilerCodes.Count -lt 8) { $compilerCodes.Add($code) }
|
|
146
|
+
}
|
|
147
|
+
foreach ($record in $captured) {
|
|
148
|
+
$exception = Get-BootstrapProperty $record 'Exception'
|
|
149
|
+
for ($depth = 0; $depth -lt 8 -and $exception -is [System.Exception]; $depth++) {
|
|
150
|
+
$message = Get-BootstrapProperty $exception 'Message'
|
|
151
|
+
if ($message -is [string]) { Add-BootstrapDiagnosticText $text $message }
|
|
152
|
+
$next = Get-BootstrapProperty $exception 'InnerException'
|
|
153
|
+
$exception = if ($next -is [System.Exception]) { $next } else { $null }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
foreach ($match in [regex]::Matches($text.ToString(), 'CS[0-9]{4}')) {
|
|
157
|
+
$code = $match.Value
|
|
158
|
+
if ($seenCompilerCodes.Add($code) -and $compilerCodes.Count -lt 8) { $compilerCodes.Add($code) }
|
|
159
|
+
}
|
|
160
|
+
if ($compilerCodes.Count -gt 0) {
|
|
161
|
+
$category = 'compiler'
|
|
162
|
+
if ($reason -eq 'unknown') { $reason = 'compiler-errors' }
|
|
163
|
+
}
|
|
164
|
+
$languageMode = Get-BootstrapLanguageMode
|
|
165
|
+
[Console]::Error.WriteLine(([pscustomobject]@{ kind = 'windows-session-bootstrap-diagnostic'; category = $category; compilerCodes = @($compilerCodes.ToArray()); reason = $reason; languageMode = $languageMode } | ConvertTo-Json -Compress))
|
|
166
|
+
} catch { Write-BootstrapDiagnosticFallback }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
$addTypeErrors = @()
|
|
170
|
+
try {
|
|
171
|
+
Add-Type -ErrorAction Stop -ErrorVariable +addTypeErrors -TypeDefinition @'
|
|
172
|
+
using System;
|
|
173
|
+
using System.Collections.Generic;
|
|
174
|
+
using System.ComponentModel;
|
|
175
|
+
using System.IO;
|
|
176
|
+
using System.Runtime.InteropServices;
|
|
177
|
+
using System.Security.AccessControl;
|
|
178
|
+
using System.Security.Principal;
|
|
179
|
+
using Microsoft.Win32.SafeHandles;
|
|
180
|
+
using System.Text;
|
|
181
|
+
using System.Text.RegularExpressions;
|
|
182
|
+
using System.Threading;
|
|
183
|
+
using System.IO.Pipes;
|
|
184
|
+
|
|
185
|
+
public sealed class BootstrapFailure : Exception {
|
|
186
|
+
public readonly string Code, Stage;
|
|
187
|
+
public readonly uint? NtStatus;
|
|
188
|
+
public BootstrapFailure(string code, string stage, uint? ntStatus) { Code = code; Stage = stage; NtStatus = ntStatus; }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public static class WindowsSessionBootstrap {
|
|
192
|
+
[StructLayout(LayoutKind.Sequential)] struct UNICODE_STRING { public ushort Length, MaximumLength; public IntPtr Buffer; }
|
|
193
|
+
[StructLayout(LayoutKind.Sequential)] struct OBJECT_ATTRIBUTES { public uint Length; public IntPtr RootDirectory, ObjectName; public uint Attributes; public IntPtr SecurityDescriptor, SecurityQualityOfService; }
|
|
194
|
+
[StructLayout(LayoutKind.Sequential)] struct IO_STATUS_BLOCK { public IntPtr Status; public UIntPtr Information; }
|
|
195
|
+
[StructLayout(LayoutKind.Sequential)] struct BY_HANDLE_FILE_INFORMATION {
|
|
196
|
+
public uint FileAttributes; public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime, LastAccessTime, LastWriteTime;
|
|
197
|
+
public uint VolumeSerialNumber, FileSizeHigh, FileSizeLow, NumberOfLinks, FileIndexHigh, FileIndexLow;
|
|
198
|
+
}
|
|
199
|
+
// FILE_ID_BOTH_DIR_INFORMATION layout from Microsoft's ntifs.h / winternl documentation.
|
|
200
|
+
[StructLayout(LayoutKind.Sequential)] struct FILE_ID_BOTH_DIR_HEADER {
|
|
201
|
+
public uint NextEntryOffset, FileIndex; public long CreationTime, LastAccessTime, LastWriteTime, ChangeTime, EndOfFile, AllocationSize;
|
|
202
|
+
public uint FileAttributes, FileNameLength, EaSize; public byte ShortNameLength;
|
|
203
|
+
[MarshalAs(UnmanagedType.ByValArray, SizeConst=24)] public byte[] ShortName; public long FileId; public ushort FileName;
|
|
204
|
+
}
|
|
205
|
+
struct OpenResult { public IntPtr Handle; public uint Status; public OpenResult(IntPtr handle, uint status) { Handle = handle; Status = status; } }
|
|
206
|
+
|
|
207
|
+
const uint OBJ_CASE_INSENSITIVE = 0x40, OBJ_DONT_REPARSE = 0x1000;
|
|
208
|
+
const uint FILE_LIST_DIRECTORY = 1, FILE_TRAVERSE = 0x20, FILE_READ_DATA = 1, FILE_WRITE_DATA = 2, FILE_READ_ATTRIBUTES = 0x80, READ_CONTROL = 0x00020000, DELETE = 0x00010000, SYNCHRONIZE = 0x00100000;
|
|
209
|
+
// Directory pins omit delete sharing; a retained published file permits replacement.
|
|
210
|
+
const uint FILE_SHARE_READ = 1, FILE_SHARE_WRITE = 2, FILE_SHARE_DELETE = 4;
|
|
211
|
+
const uint FILE_OPEN = 1, FILE_CREATE = 2, FILE_DIRECTORY_FILE = 1, FILE_NON_DIRECTORY_FILE = 0x40, FILE_SYNCHRONOUS_IO_NONALERT = 0x20, FILE_OPEN_REPARSE_POINT = 0x00200000;
|
|
212
|
+
const uint FILE_ATTRIBUTE_DIRECTORY = 0x10, FILE_ATTRIBUTE_REPARSE_POINT = 0x400;
|
|
213
|
+
const uint SE_FILE_OBJECT = 1, OWNER_SECURITY_INFORMATION = 1, DACL_SECURITY_INFORMATION = 4;
|
|
214
|
+
const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034, STATUS_OBJECT_NAME_COLLISION = 0xC0000035;
|
|
215
|
+
const int FileIdBothDirectoryInformation = 37;
|
|
216
|
+
static readonly object Gate = new object();
|
|
217
|
+
static readonly object OutputGate = new object();
|
|
218
|
+
static readonly List<IntPtr> Handles = new List<IntPtr>();
|
|
219
|
+
static IntPtr Presence = IntPtr.Zero;
|
|
220
|
+
static string InitializationStage = "unknown";
|
|
221
|
+
static uint? InitializationNtStatus = null;
|
|
222
|
+
static OwnedListener Listener = null;
|
|
223
|
+
|
|
224
|
+
[DllImport("ntdll.dll", CallingConvention=CallingConvention.Winapi)] static extern uint NtCreateFile(out IntPtr fileHandle, uint desiredAccess, ref OBJECT_ATTRIBUTES objectAttributes, out IO_STATUS_BLOCK ioStatusBlock, IntPtr allocationSize, uint fileAttributes, uint shareAccess, uint createDisposition, uint createOptions, IntPtr eaBuffer, uint eaLength);
|
|
225
|
+
[DllImport("ntdll.dll", CallingConvention=CallingConvention.Winapi)] static extern uint NtQueryDirectoryFile(IntPtr fileHandle, IntPtr eventHandle, IntPtr apcRoutine, IntPtr apcContext, out IO_STATUS_BLOCK ioStatusBlock, IntPtr fileInformation, uint length, int fileInformationClass, bool returnSingleEntry, IntPtr fileName, bool restartScan);
|
|
226
|
+
[DllImport("ntdll.dll", CallingConvention=CallingConvention.Winapi)] static extern uint NtSetInformationFile(IntPtr fileHandle, out IO_STATUS_BLOCK ioStatusBlock, IntPtr fileInformation, uint length, int fileInformationClass);
|
|
227
|
+
[DllImport("ntdll.dll", CallingConvention=CallingConvention.Winapi)] static extern uint RtlNtStatusToDosError(uint status);
|
|
228
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool CloseHandle(IntPtr handle);
|
|
229
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool GetFileInformationByHandle(IntPtr handle, out BY_HANDLE_FILE_INFORMATION info);
|
|
230
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool ReadFile(IntPtr handle, byte[] buffer, uint length, out uint read, IntPtr overlapped);
|
|
231
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool WriteFile(IntPtr handle, byte[] buffer, uint length, out uint written, IntPtr overlapped);
|
|
232
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool FlushFileBuffers(IntPtr handle);
|
|
233
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateNamedPipe(string name, uint openMode, uint pipeMode, uint maxInstances, uint outBufferSize, uint inBufferSize, uint defaultTimeout, IntPtr securityAttributes);
|
|
234
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool GetVolumeNameForVolumeMountPoint(string rootPathName, System.Text.StringBuilder volumeName, uint cchBufferLength);
|
|
235
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool GetVolumeInformation(string rootPathName, IntPtr volumeNameBuffer, uint volumeNameSize, out uint volumeSerialNumber, out uint maximumComponentLength, out uint fileSystemFlags, IntPtr fileSystemNameBuffer, uint fileSystemNameSize);
|
|
236
|
+
[DllImport("advapi32.dll", SetLastError=true)] static extern uint GetSecurityInfo(IntPtr handle, uint objectType, uint securityInformation, out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr descriptor);
|
|
237
|
+
[DllImport("advapi32.dll")] static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
|
|
238
|
+
[DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr memory);
|
|
239
|
+
|
|
240
|
+
static void Fail(string code) { throw new BootstrapFailure(code, InitializationStage, InitializationNtStatus); }
|
|
241
|
+
static void SetStage(string stage) { InitializationStage = stage; InitializationNtStatus = null; }
|
|
242
|
+
static void SetNtStatus(uint status) { InitializationNtStatus = status; }
|
|
243
|
+
static void Close(IntPtr handle) { if (handle != IntPtr.Zero) CloseHandle(handle); }
|
|
244
|
+
public static void WriteControl(string line) { lock (OutputGate) { Console.Out.WriteLine(line); } }
|
|
245
|
+
static IntPtr Unicode(string value, out IntPtr chars) {
|
|
246
|
+
chars = Marshal.StringToHGlobalUni(value); var text = new UNICODE_STRING();
|
|
247
|
+
text.Length = checked((ushort)(value.Length * 2)); text.MaximumLength = text.Length; text.Buffer = chars;
|
|
248
|
+
IntPtr valuePointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(UNICODE_STRING))); Marshal.StructureToPtr(text, valuePointer, false); return valuePointer;
|
|
249
|
+
}
|
|
250
|
+
static OpenResult Open(IntPtr root, string name, bool privateDirectory, bool create, byte[] descriptor) {
|
|
251
|
+
IntPtr chars = IntPtr.Zero, unicode = IntPtr.Zero, security = IntPtr.Zero, handle = IntPtr.Zero;
|
|
252
|
+
try {
|
|
253
|
+
unicode = Unicode(name, out chars); var attributes = new OBJECT_ATTRIBUTES();
|
|
254
|
+
attributes.Length = (uint)Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)); attributes.RootDirectory = root; attributes.ObjectName = unicode;
|
|
255
|
+
attributes.Attributes = OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE;
|
|
256
|
+
if (create) { if (descriptor == null || descriptor.Length == 0) Fail("unavailable"); security = Marshal.AllocHGlobal(descriptor.Length); Marshal.Copy(descriptor, 0, security, descriptor.Length); attributes.SecurityDescriptor = security; }
|
|
257
|
+
uint access = SYNCHRONIZE | FILE_TRAVERSE | FILE_READ_ATTRIBUTES;
|
|
258
|
+
if (privateDirectory) access |= FILE_LIST_DIRECTORY | READ_CONTROL;
|
|
259
|
+
IO_STATUS_BLOCK statusBlock; uint status = NtCreateFile(out handle, access, ref attributes, out statusBlock, IntPtr.Zero, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, create ? FILE_CREATE : FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT, IntPtr.Zero, 0);
|
|
260
|
+
if (status != 0) { Close(handle); return new OpenResult(IntPtr.Zero, status); }
|
|
261
|
+
return new OpenResult(handle, status);
|
|
262
|
+
} finally { if (security != IntPtr.Zero) Marshal.FreeHGlobal(security); if (unicode != IntPtr.Zero) Marshal.FreeHGlobal(unicode); if (chars != IntPtr.Zero) Marshal.FreeHGlobal(chars); }
|
|
263
|
+
}
|
|
264
|
+
// OBJ_DONT_REPARSE returns STATUS_REPARSE_POINT_ENCOUNTERED (0xC000050B) before file-system parsing.
|
|
265
|
+
// The canonical Volume GUID name is returned by the OS for the validated drive root, so only this opener
|
|
266
|
+
// permits that Object Manager alias resolution; every user-derived component still uses Open above.
|
|
267
|
+
static OpenResult OpenVolume(string volumePath) {
|
|
268
|
+
IntPtr chars = IntPtr.Zero, unicode = IntPtr.Zero, handle = IntPtr.Zero;
|
|
269
|
+
try {
|
|
270
|
+
unicode = Unicode(volumePath, out chars); var attributes = new OBJECT_ATTRIBUTES();
|
|
271
|
+
attributes.Length = (uint)Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)); attributes.RootDirectory = IntPtr.Zero; attributes.ObjectName = unicode;
|
|
272
|
+
attributes.Attributes = OBJ_CASE_INSENSITIVE;
|
|
273
|
+
uint access = SYNCHRONIZE | FILE_TRAVERSE | FILE_READ_ATTRIBUTES;
|
|
274
|
+
IO_STATUS_BLOCK statusBlock; uint status = NtCreateFile(out handle, access, ref attributes, out statusBlock, IntPtr.Zero, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT, IntPtr.Zero, 0);
|
|
275
|
+
if (status != 0) { Close(handle); return new OpenResult(IntPtr.Zero, status); }
|
|
276
|
+
return new OpenResult(handle, status);
|
|
277
|
+
} finally { if (unicode != IntPtr.Zero) Marshal.FreeHGlobal(unicode); if (chars != IntPtr.Zero) Marshal.FreeHGlobal(chars); }
|
|
278
|
+
}
|
|
279
|
+
static void AssertDirectory(IntPtr handle) {
|
|
280
|
+
if (handle == IntPtr.Zero) Fail("unsafe");
|
|
281
|
+
BY_HANDLE_FILE_INFORMATION info;
|
|
282
|
+
if (!GetFileInformationByHandle(handle, out info)) Fail("unsafe");
|
|
283
|
+
if ((info.FileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != FILE_ATTRIBUTE_DIRECTORY) Fail("unsafe");
|
|
284
|
+
}
|
|
285
|
+
static IntPtr RequireVolumeOpen(string volumePath, uint volumeSerial) {
|
|
286
|
+
OpenResult result = OpenVolume(volumePath);
|
|
287
|
+
if (result.Handle == IntPtr.Zero) { SetNtStatus(result.Status); Fail(result.Status == STATUS_OBJECT_NAME_NOT_FOUND ? "unavailable" : "unsafe"); }
|
|
288
|
+
try {
|
|
289
|
+
AssertDirectory(result.Handle);
|
|
290
|
+
BY_HANDLE_FILE_INFORMATION info;
|
|
291
|
+
if (!GetFileInformationByHandle(result.Handle, out info) || info.VolumeSerialNumber != volumeSerial) Fail("unsafe");
|
|
292
|
+
return result.Handle;
|
|
293
|
+
} catch { Close(result.Handle); throw; }
|
|
294
|
+
}
|
|
295
|
+
static IntPtr RequireOpen(IntPtr root, string component, bool privateDirectory) {
|
|
296
|
+
OpenResult result = Open(root, component, privateDirectory, false, null);
|
|
297
|
+
if (result.Handle == IntPtr.Zero) { SetNtStatus(result.Status); Fail(result.Status == STATUS_OBJECT_NAME_NOT_FOUND ? "unavailable" : "unsafe"); }
|
|
298
|
+
try { AssertDirectory(result.Handle); return result.Handle; } catch { Close(result.Handle); throw; }
|
|
299
|
+
}
|
|
300
|
+
static void AssertOwned(IntPtr handle, string sid) {
|
|
301
|
+
IntPtr owner, group, dacl, sacl, descriptor = IntPtr.Zero;
|
|
302
|
+
uint status = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, out owner, out group, out dacl, out sacl, out descriptor);
|
|
303
|
+
if (status != 0 || descriptor == IntPtr.Zero) Fail("unsafe");
|
|
304
|
+
try {
|
|
305
|
+
uint length = GetSecurityDescriptorLength(descriptor); if (length == 0 || length > 65536) Fail("unsafe");
|
|
306
|
+
byte[] bytes = new byte[length]; Marshal.Copy(descriptor, bytes, 0, (int)length); var raw = new RawSecurityDescriptor(bytes, 0);
|
|
307
|
+
if (raw.Owner == null || raw.Owner.Value != sid || (raw.ControlFlags & ControlFlags.DiscretionaryAclProtected) == 0 || raw.DiscretionaryAcl == null || raw.DiscretionaryAcl.Count != 1) Fail("unsafe");
|
|
308
|
+
CommonAce ace = raw.DiscretionaryAcl[0] as CommonAce;
|
|
309
|
+
if (ace == null || ace.IsCallback || ace.AceFlags != AceFlags.None || ace.AceQualifier != AceQualifier.AccessAllowed || ace.IsInherited || ace.SecurityIdentifier == null || ace.SecurityIdentifier.Value != sid || ace.AccessMask != 0x1F01FF) Fail("unsafe");
|
|
310
|
+
} finally { LocalFree(descriptor); }
|
|
311
|
+
}
|
|
312
|
+
static IntPtr CreateOrOpenOwned(IntPtr parent, string name, byte[] descriptor, string sid, string createStage, string assertStage) {
|
|
313
|
+
SetStage(createStage);
|
|
314
|
+
OpenResult created = Open(parent, name, true, true, descriptor); IntPtr handle = created.Handle;
|
|
315
|
+
if (handle == IntPtr.Zero) {
|
|
316
|
+
if (created.Status != STATUS_OBJECT_NAME_COLLISION) { SetNtStatus(created.Status); Fail(created.Status == STATUS_OBJECT_NAME_NOT_FOUND ? "unavailable" : "unsafe"); }
|
|
317
|
+
handle = RequireOpen(parent, name, true);
|
|
318
|
+
}
|
|
319
|
+
try { SetStage(assertStage); AssertDirectory(handle); AssertOwned(handle, sid); return handle; } catch { Close(handle); throw; }
|
|
320
|
+
}
|
|
321
|
+
static string[] Components(string agentHome) {
|
|
322
|
+
if (String.IsNullOrEmpty(agentHome) || agentHome.Length > 4096 || !Regex.IsMatch(agentHome, @"^[A-Za-z]:\\(?:[^\\]+\\)*[^\\]+$") /* "^[A-Za-z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+$")) */ ) Fail("invalid");
|
|
323
|
+
if (agentHome.StartsWith("\\\\") || agentHome.IndexOf(':', 2) >= 0 || agentHome.IndexOf("\0") >= 0) Fail("invalid");
|
|
324
|
+
string[] parts = agentHome.Substring(3).Split('\\'); if (parts.Length == 0) Fail("invalid");
|
|
325
|
+
foreach (string part in parts) if (part.Length == 0 || part == "." || part == ".." || part.IndexOf(':') >= 0 || part.IndexOfAny(new char[] {'/', '\0'}) >= 0) Fail("invalid");
|
|
326
|
+
return parts;
|
|
327
|
+
}
|
|
328
|
+
static string VolumePath(string agentHome, out uint volumeSerial) {
|
|
329
|
+
var name = new System.Text.StringBuilder(128); string mount = agentHome.Substring(0, 3);
|
|
330
|
+
if (!GetVolumeNameForVolumeMountPoint(mount, name, (uint)name.Capacity)) Fail("unavailable");
|
|
331
|
+
string volume = name.ToString(); if (!Regex.IsMatch(volume, @"^\\\\\?\\Volume\{[0-9A-Fa-f-]+\}\\$") /* "^\\\\\\?\\\\Volume\\{[0-9A-Fa-f-]+\\}\\\\$")) */ ) Fail("unsafe");
|
|
332
|
+
uint maximumComponentLength, fileSystemFlags;
|
|
333
|
+
if (!GetVolumeInformation(volume, IntPtr.Zero, 0, out volumeSerial, out maximumComponentLength, out fileSystemFlags, IntPtr.Zero, 0)) Fail("unavailable");
|
|
334
|
+
return @"\??\" + volume.Substring(4);
|
|
335
|
+
}
|
|
336
|
+
// The helper keeps this capability local. Publication/list RPCs are intentionally absent.
|
|
337
|
+
static string[] EnumeratePinned(IntPtr directory) {
|
|
338
|
+
const int MaxEntries = 64, MaxBytes = 8192; const uint STATUS_NO_MORE_FILES = 0x80000006;
|
|
339
|
+
int Header = Marshal.OffsetOf(typeof(FILE_ID_BOTH_DIR_HEADER), "FileName").ToInt32(); if (Header <= 0 || Header >= MaxBytes) Fail("unavailable"); var names = new List<string>(); IntPtr buffer = Marshal.AllocHGlobal(MaxBytes);
|
|
340
|
+
try {
|
|
341
|
+
bool restart = true;
|
|
342
|
+
for (;;) {
|
|
343
|
+
IO_STATUS_BLOCK io; uint status = NtQueryDirectoryFile(directory, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out io, buffer, MaxBytes, FileIdBothDirectoryInformation, false, IntPtr.Zero, restart); restart = false;
|
|
344
|
+
if (status == STATUS_NO_MORE_FILES) return names.ToArray();
|
|
345
|
+
if (status != 0) Fail("unsafe");
|
|
346
|
+
ulong available64 = io.Information.ToUInt64(); if (available64 == 0 || available64 > MaxBytes) Fail("unsafe"); int available = (int)available64;
|
|
347
|
+
int offset = 0;
|
|
348
|
+
while (offset < available) {
|
|
349
|
+
if (available - offset < Header) Fail("unsafe");
|
|
350
|
+
int next = Marshal.ReadInt32(buffer, offset), nameLength = Marshal.ReadInt32(buffer, offset + 60);
|
|
351
|
+
int recordLength = next == 0 ? available - offset : next;
|
|
352
|
+
if (recordLength < Header || recordLength > available - offset || (next != 0 && (next & 7) != 0) || nameLength < 0 || (nameLength & 1) != 0 || nameLength > recordLength - Header) Fail("unsafe");
|
|
353
|
+
string name = Marshal.PtrToStringUni(IntPtr.Add(buffer, offset + Header), nameLength / 2);
|
|
354
|
+
if (String.IsNullOrEmpty(name) || name.IndexOfAny(new char[] {'\\', '/', ':', '\0'}) >= 0) Fail("unsafe");
|
|
355
|
+
if (name != "." && name != "..") { if (names.Count >= MaxEntries) Fail("busy"); names.Add(name); }
|
|
356
|
+
if (next == 0) { offset = available; } else { offset += next; }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} finally { Marshal.FreeHGlobal(buffer); }
|
|
360
|
+
}
|
|
361
|
+
public static int EnumeratePresence() { lock (Gate) { if (Presence == IntPtr.Zero) Fail("unavailable"); return EnumeratePinned(Presence).Length; } }
|
|
362
|
+
public sealed class PresenceRecord {
|
|
363
|
+
public readonly string SessionId, Endpoint, Token; public readonly long CreatedAt;
|
|
364
|
+
public PresenceRecord(string sessionId, string endpoint, string token, long createdAt) { SessionId = sessionId; Endpoint = endpoint; Token = token; CreatedAt = createdAt; }
|
|
365
|
+
}
|
|
366
|
+
static readonly string PipePrefix = @"\\.\pipe\gentle-pi-";
|
|
367
|
+
const int FileDispositionInformation = 13, MaxOwnedPublications = 64;
|
|
368
|
+
struct RecordIdentity { public uint Volume; public uint IndexHigh, IndexLow; public RecordIdentity(BY_HANDLE_FILE_INFORMATION info) { Volume = info.VolumeSerialNumber; IndexHigh = info.FileIndexHigh; IndexLow = info.FileIndexLow; } public bool Equals(RecordIdentity other) { return Volume == other.Volume && IndexHigh == other.IndexHigh && IndexLow == other.IndexLow; } }
|
|
369
|
+
sealed class OwnedPublication { public readonly PresenceRecord Record; public readonly RecordIdentity Identity; public readonly string Sid; public SafeFileHandle Handle; public OwnedPublication(PresenceRecord record, RecordIdentity identity, string sid, IntPtr handle) { Record = record; Identity = identity; Sid = sid; Handle = new SafeFileHandle(handle, true); } public void Dispose() { if (Handle != null) { Handle.Dispose(); Handle = null; } } }
|
|
370
|
+
static readonly List<OwnedPublication> OwnedPublications = new List<OwnedPublication>();
|
|
371
|
+
static bool SameRecord(PresenceRecord left, string sessionId, string endpoint, long createdAt) { return left.SessionId == sessionId && left.Endpoint == endpoint && left.CreatedAt == createdAt; }
|
|
372
|
+
static OwnedPublication FindOwned(string sessionId, string endpoint, long createdAt) { foreach (OwnedPublication owned in OwnedPublications) if (SameRecord(owned.Record, sessionId, endpoint, createdAt)) return owned; return null; }
|
|
373
|
+
static void RequirePresence() { if (Presence == IntPtr.Zero) Fail("unavailable"); }
|
|
374
|
+
static void ValidateRecord(string sessionId, string endpoint, long createdAt, out string token) {
|
|
375
|
+
token = null;
|
|
376
|
+
if (String.IsNullOrEmpty(sessionId) || !Regex.IsMatch(sessionId, "^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$") || createdAt < 0 || createdAt > 9007199254740991L || String.IsNullOrEmpty(endpoint) || !endpoint.StartsWith(PipePrefix, StringComparison.Ordinal)) Fail("invalid");
|
|
377
|
+
token = endpoint.Substring(PipePrefix.Length); if (!Regex.IsMatch(token, "^[0-9a-f]{32}$")) Fail("invalid");
|
|
378
|
+
}
|
|
379
|
+
static string RecordName(string sessionId, string token) { return sessionId + "." + token + ".json"; }
|
|
380
|
+
static string RecordText(string sessionId, string endpoint, long createdAt) { return "{\"version\":1,\"sessionId\":\"" + sessionId + "\",\"endpoint\":\"" + endpoint.Replace("\\", "\\\\") + "\",\"createdAt\":" + createdAt.ToString(System.Globalization.CultureInfo.InvariantCulture) + "}"; }
|
|
381
|
+
static OpenResult OpenRecord(IntPtr root, string name, bool create, byte[] descriptor, bool allowDeleteSharing) {
|
|
382
|
+
IntPtr chars = IntPtr.Zero, unicode = IntPtr.Zero, security = IntPtr.Zero, handle = IntPtr.Zero;
|
|
383
|
+
try {
|
|
384
|
+
unicode = Unicode(name, out chars); var attributes = new OBJECT_ATTRIBUTES(); attributes.Length = (uint)Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)); attributes.RootDirectory = root; attributes.ObjectName = unicode; attributes.Attributes = OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE;
|
|
385
|
+
if (create) { if (descriptor == null || descriptor.Length == 0) Fail("unavailable"); security = Marshal.AllocHGlobal(descriptor.Length); Marshal.Copy(descriptor, 0, security, descriptor.Length); attributes.SecurityDescriptor = security; }
|
|
386
|
+
IO_STATUS_BLOCK statusBlock; uint status = NtCreateFile(out handle, SYNCHRONIZE | FILE_READ_DATA | FILE_WRITE_DATA | FILE_READ_ATTRIBUTES | READ_CONTROL | DELETE, ref attributes, out statusBlock, IntPtr.Zero, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | (allowDeleteSharing ? FILE_SHARE_DELETE : 0), create ? FILE_CREATE : FILE_OPEN, FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT, IntPtr.Zero, 0);
|
|
387
|
+
if (status != 0) { Close(handle); return new OpenResult(IntPtr.Zero, status); } return new OpenResult(handle, status);
|
|
388
|
+
} finally { if (security != IntPtr.Zero) Marshal.FreeHGlobal(security); if (unicode != IntPtr.Zero) Marshal.FreeHGlobal(unicode); if (chars != IntPtr.Zero) Marshal.FreeHGlobal(chars); }
|
|
389
|
+
}
|
|
390
|
+
static RecordIdentity AssertRecord(IntPtr handle, string sid) {
|
|
391
|
+
BY_HANDLE_FILE_INFORMATION info; if (!GetFileInformationByHandle(handle, out info) || info.NumberOfLinks != 1 || (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 || (info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) Fail("unsafe"); AssertOwned(handle, sid); return new RecordIdentity(info);
|
|
392
|
+
}
|
|
393
|
+
static RecordIdentity AssertRetainedPublication(SafeFileHandle handle, string sid) {
|
|
394
|
+
if (handle == null || handle.IsInvalid || handle.IsClosed) Fail("unsafe");
|
|
395
|
+
BY_HANDLE_FILE_INFORMATION info; IntPtr value = handle.DangerousGetHandle();
|
|
396
|
+
if (!GetFileInformationByHandle(value, out info) || (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 || (info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) Fail("unsafe");
|
|
397
|
+
AssertOwned(value, sid); return new RecordIdentity(info);
|
|
398
|
+
}
|
|
399
|
+
static string ReadRecordText(IntPtr handle) {
|
|
400
|
+
BY_HANDLE_FILE_INFORMATION info; if (!GetFileInformationByHandle(handle, out info) || info.FileSizeHigh != 0 || info.FileSizeLow > 8192) Fail("invalid"); byte[] bytes = new byte[info.FileSizeLow]; uint read;
|
|
401
|
+
if (!ReadFile(handle, bytes, info.FileSizeLow, out read, IntPtr.Zero) || read != info.FileSizeLow) Fail("invalid"); try { return new UTF8Encoding(false, true).GetString(bytes); } catch { Fail("invalid"); return null; }
|
|
402
|
+
}
|
|
403
|
+
static PresenceRecord ParseRecord(string name, string text) {
|
|
404
|
+
int first = name.IndexOf('.'), second = first < 0 ? -1 : name.IndexOf('.', first + 1); if (first <= 0 || second <= first + 1 || second != name.Length - 5 || !name.EndsWith(".json", StringComparison.Ordinal)) Fail("invalid");
|
|
405
|
+
string sessionId = name.Substring(0, first), token = name.Substring(first + 1, second - first - 1), endpoint = PipePrefix + token; string ignored; ValidateRecord(sessionId, endpoint, 0, out ignored);
|
|
406
|
+
string prefix = "{\"version\":1,\"sessionId\":\"" + sessionId + "\",\"endpoint\":\"" + endpoint.Replace("\\", "\\\\") + "\",\"createdAt\":"; if (!text.StartsWith(prefix, StringComparison.Ordinal) || !text.EndsWith("}", StringComparison.Ordinal)) Fail("invalid");
|
|
407
|
+
string number = text.Substring(prefix.Length, text.Length - prefix.Length - 1); if (number.Length == 0 || number.Length > 16) Fail("invalid"); long createdAt = 0; if (!Int64.TryParse(number, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out createdAt)) Fail("invalid"); ValidateRecord(sessionId, endpoint, createdAt, out ignored); return new PresenceRecord(sessionId, endpoint, token, createdAt);
|
|
408
|
+
}
|
|
409
|
+
static PresenceRecord ReadPinnedRecord(string name, string sid, out IntPtr handle, out RecordIdentity identity) {
|
|
410
|
+
handle = IntPtr.Zero; identity = new RecordIdentity(); OpenResult opened = OpenRecord(Presence, name, false, null, true); if (opened.Handle == IntPtr.Zero) { SetNtStatus(opened.Status); Fail(opened.Status == STATUS_OBJECT_NAME_NOT_FOUND ? "not_found" : "unsafe"); }
|
|
411
|
+
handle = opened.Handle; try { identity = AssertRecord(handle, sid); return ParseRecord(name, ReadRecordText(handle)); } catch { Close(handle); handle = IntPtr.Zero; throw; }
|
|
412
|
+
}
|
|
413
|
+
static uint RenameNoReplace(IntPtr file, IntPtr root, string name) {
|
|
414
|
+
byte[] chars = Encoding.Unicode.GetBytes(name); int header = IntPtr.Size == 8 ? 20 : 12; IntPtr memory = Marshal.AllocHGlobal(header + chars.Length); try { for (int index = 0; index < header + chars.Length; index++) Marshal.WriteByte(memory, index, 0); Marshal.WriteIntPtr(memory, IntPtr.Size == 8 ? 8 : 4, root); Marshal.WriteInt32(memory, IntPtr.Size == 8 ? 16 : 8, chars.Length); Marshal.Copy(chars, 0, IntPtr.Add(memory, header), chars.Length); IO_STATUS_BLOCK io; return NtSetInformationFile(file, out io, memory, (uint)(header + chars.Length), 10); } finally { Marshal.FreeHGlobal(memory); }
|
|
415
|
+
}
|
|
416
|
+
static void DeletePinnedHandle(IntPtr file) { IntPtr memory = Marshal.AllocHGlobal(1); try { Marshal.WriteByte(memory, 1); IO_STATUS_BLOCK io; if (NtSetInformationFile(file, out io, memory, 1, FileDispositionInformation) != 0) Fail("unsafe"); } finally { Marshal.FreeHGlobal(memory); } }
|
|
417
|
+
public static PresenceRecord NewRecord(string sessionId, long createdAt) { lock (Gate) { RequirePresence(); string token = Guid.NewGuid().ToString("N"), endpoint = PipePrefix + token, ignored; ValidateRecord(sessionId, endpoint, createdAt, out ignored); return new PresenceRecord(sessionId, endpoint, token, createdAt); } }
|
|
418
|
+
static OwnedPublication PublishOwned(string sessionId, string endpoint, long createdAt, byte[] descriptor, string sid) {
|
|
419
|
+
lock (Gate) {
|
|
420
|
+
RequirePresence(); if (OwnedPublications.Count >= MaxOwnedPublications) Fail("busy");
|
|
421
|
+
string token; ValidateRecord(sessionId, endpoint, createdAt, out token);
|
|
422
|
+
string temporary = "." + Guid.NewGuid().ToString("N") + ".tmp", target = RecordName(sessionId, token);
|
|
423
|
+
OpenResult opened = OpenRecord(Presence, temporary, true, descriptor, true);
|
|
424
|
+
if (opened.Handle == IntPtr.Zero) { SetNtStatus(opened.Status); Fail(opened.Status == STATUS_OBJECT_NAME_COLLISION ? "busy" : "unsafe"); }
|
|
425
|
+
IntPtr file = opened.Handle; bool transferred = false;
|
|
426
|
+
try {
|
|
427
|
+
RecordIdentity identity = AssertRecord(file, sid);
|
|
428
|
+
byte[] bytes = new UTF8Encoding(false).GetBytes(RecordText(sessionId, endpoint, createdAt)); uint written;
|
|
429
|
+
if (bytes.Length > 8192 || !WriteFile(file, bytes, (uint)bytes.Length, out written, IntPtr.Zero) || written != bytes.Length || !FlushFileBuffers(file)) Fail("unavailable");
|
|
430
|
+
uint status = RenameNoReplace(file, Presence, target);
|
|
431
|
+
if (status != 0) { uint win32 = RtlNtStatusToDosError(status); Fail(win32 == 80 || win32 == 183 ? "busy" : "unsafe"); }
|
|
432
|
+
OwnedPublication owned = new OwnedPublication(new PresenceRecord(sessionId, endpoint, token, createdAt), identity, sid, file);
|
|
433
|
+
try { OwnedPublications.Add(owned); transferred = true; return owned; } catch { try { DeletePinnedHandle(file); } finally { owned.Dispose(); transferred = true; } throw; }
|
|
434
|
+
} finally {
|
|
435
|
+
if (!transferred) { try { DeletePinnedHandle(file); } catch {} Close(file); }
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
public static void Publish(string sessionId, string endpoint, long createdAt, byte[] descriptor, string sid) { PublishOwned(sessionId, endpoint, createdAt, descriptor, sid); }
|
|
440
|
+
public static PresenceRecord[] List(string sid) { return List(null, sid); }
|
|
441
|
+
public static PresenceRecord[] List(string excluded, string sid) { lock (Gate) { RequirePresence(); if (excluded != null && !Regex.IsMatch(excluded, "^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$")) Fail("invalid"); var newest = new Dictionary<string, PresenceRecord>(StringComparer.Ordinal); foreach (string name in EnumeratePinned(Presence)) { IntPtr file = IntPtr.Zero; try { RecordIdentity ignoredIdentity; PresenceRecord record = ReadPinnedRecord(name, sid, out file, out ignoredIdentity); if (record.SessionId == excluded) continue; PresenceRecord prior; if (!newest.TryGetValue(record.SessionId, out prior) || record.CreatedAt > prior.CreatedAt || (record.CreatedAt == prior.CreatedAt && String.CompareOrdinal(record.Token, prior.Token) > 0)) newest[record.SessionId] = record; } catch (BootstrapFailure) { } finally { Close(file); } } var records = new List<PresenceRecord>(newest.Values); records.Sort(delegate(PresenceRecord left, PresenceRecord right) { return String.CompareOrdinal(left.SessionId, right.SessionId); }); return records.ToArray(); } }
|
|
442
|
+
public static PresenceRecord Resolve(string sessionId, string sid) { PresenceRecord[] records = List(null, sid); foreach (PresenceRecord record in records) if (record.SessionId == sessionId) return record; Fail("not_found"); return null; }
|
|
443
|
+
static void RemoveOwnedPublication(OwnedPublication owned) {
|
|
444
|
+
IntPtr current = IntPtr.Zero;
|
|
445
|
+
try {
|
|
446
|
+
RecordIdentity retained = AssertRetainedPublication(owned.Handle, owned.Sid);
|
|
447
|
+
if (!retained.Equals(owned.Identity)) Fail("unsafe");
|
|
448
|
+
RecordIdentity currentIdentity; PresenceRecord record = ReadPinnedRecord(RecordName(owned.Record.SessionId, owned.Record.Token), owned.Sid, out current, out currentIdentity);
|
|
449
|
+
if (!SameRecord(record, owned.Record.SessionId, owned.Record.Endpoint, owned.Record.CreatedAt) || !currentIdentity.Equals(retained)) Fail("unsafe");
|
|
450
|
+
DeletePinnedHandle(current);
|
|
451
|
+
} catch (BootstrapFailure failure) { if (failure.Code != "not_found") throw; }
|
|
452
|
+
finally { Close(current); OwnedPublications.Remove(owned); owned.Dispose(); }
|
|
453
|
+
}
|
|
454
|
+
static void RemoveListenerPublication(OwnedListener listener) {
|
|
455
|
+
if (listener == null) return;
|
|
456
|
+
OwnedPublication owned = listener.Publication; listener.Publication = null;
|
|
457
|
+
// Reference membership, rather than the public tuple, prevents this listener
|
|
458
|
+
// from deleting a later publication that reused the same visible record.
|
|
459
|
+
if (owned == null || !OwnedPublications.Contains(owned)) return;
|
|
460
|
+
RemoveOwnedPublication(owned);
|
|
461
|
+
}
|
|
462
|
+
public static void RemoveOwn(string sessionId, string endpoint, long createdAt, string sid) {
|
|
463
|
+
lock (Gate) {
|
|
464
|
+
RequirePresence(); string token; ValidateRecord(sessionId, endpoint, createdAt, out token);
|
|
465
|
+
OwnedPublication owned = FindOwned(sessionId, endpoint, createdAt); if (owned == null) return;
|
|
466
|
+
IntPtr file = IntPtr.Zero;
|
|
467
|
+
try {
|
|
468
|
+
if (owned.Sid != sid) Fail("unsafe"); RecordIdentity retained = AssertRetainedPublication(owned.Handle, owned.Sid); if (!retained.Equals(owned.Identity)) Fail("unsafe"); RecordIdentity identity; PresenceRecord record = ReadPinnedRecord(RecordName(sessionId, token), sid, out file, out identity);
|
|
469
|
+
if (!SameRecord(record, sessionId, endpoint, createdAt) || !identity.Equals(retained)) Fail("unsafe");
|
|
470
|
+
DeletePinnedHandle(file);
|
|
471
|
+
} catch (BootstrapFailure failure) { if (failure.Code != "not_found") throw; }
|
|
472
|
+
finally { Close(file); OwnedPublications.Remove(owned); owned.Dispose(); }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// CreateNamedPipeW's documented FILE_FLAG_FIRST_PIPE_INSTANCE guard avoids
|
|
476
|
+
// relying on PipeOptions.FirstPipeInstance, which PS5.1 may not expose.
|
|
477
|
+
const uint PIPE_ACCESS_DUPLEX = 3, PIPE_TYPE_BYTE = 0, PIPE_WAIT = 0, FILE_FLAG_OVERLAPPED = 0x40000000, FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000;
|
|
478
|
+
const int MaxPipeInstances = 4, MaxPipeBytes = 65536, PipeDeadlineMilliseconds = 2000;
|
|
479
|
+
sealed class PipeClient {
|
|
480
|
+
public readonly OwnedListener Listener; public readonly NamedPipeServerStream Pipe; public readonly string ConnectionId;
|
|
481
|
+
public readonly MemoryStream Bytes = new MemoryStream(); public readonly byte[] Buffer = new byte[4096]; public Timer Timer; public string Id; public bool Closed, Writing, Accepting;
|
|
482
|
+
public PipeClient(OwnedListener listener, NamedPipeServerStream pipe) { Listener = listener; Pipe = pipe; ConnectionId = Guid.NewGuid().ToString("N"); }
|
|
483
|
+
}
|
|
484
|
+
sealed class OwnedListener {
|
|
485
|
+
public readonly PresenceRecord Record; public readonly string Sid; public readonly byte[] Descriptor; public readonly int Generation;
|
|
486
|
+
public readonly List<PipeClient> Clients = new List<PipeClient>(); public OwnedPublication Publication; public bool FirstCreated; public bool Stopped;
|
|
487
|
+
public OwnedListener(PresenceRecord record, string sid, byte[] descriptor, int generation) { Record = record; Sid = sid; Descriptor = descriptor; Generation = generation; }
|
|
488
|
+
}
|
|
489
|
+
static int ListenerGeneration = 0;
|
|
490
|
+
static void AssertPipeOwned(NamedPipeServerStream pipe, string sid) {
|
|
491
|
+
PipeSecurity security = pipe.GetAccessControl();
|
|
492
|
+
SecurityIdentifier owner = security.GetOwner(typeof(SecurityIdentifier)) as SecurityIdentifier;
|
|
493
|
+
if (owner == null || owner.Value != sid || !security.AreAccessRulesProtected) Fail("unsafe");
|
|
494
|
+
AuthorizationRuleCollection rules = security.GetAccessRules(true, true, typeof(SecurityIdentifier)); if (rules.Count != 1) Fail("unsafe");
|
|
495
|
+
PipeAccessRule rule = rules[0] as PipeAccessRule;
|
|
496
|
+
if (rule == null || rule.IsInherited || rule.AccessControlType != AccessControlType.Allow || rule.IdentityReference == null || rule.IdentityReference.Value != sid || rule.PipeAccessRights != PipeAccessRights.FullControl) Fail("unsafe");
|
|
497
|
+
}
|
|
498
|
+
static NamedPipeServerStream CreateOwnedPipe(OwnedListener listener, bool first) {
|
|
499
|
+
IntPtr descriptor = IntPtr.Zero, attributes = IntPtr.Zero, handle = IntPtr.Zero;
|
|
500
|
+
try {
|
|
501
|
+
descriptor = Marshal.AllocHGlobal(listener.Descriptor.Length); Marshal.Copy(listener.Descriptor, 0, descriptor, listener.Descriptor.Length);
|
|
502
|
+
int size = IntPtr.Size == 8 ? 24 : 12; attributes = Marshal.AllocHGlobal(size); for (int index = 0; index < size; index++) Marshal.WriteByte(attributes, index, 0);
|
|
503
|
+
Marshal.WriteInt32(attributes, 0, size); Marshal.WriteIntPtr(attributes, IntPtr.Size == 8 ? 8 : 4, descriptor);
|
|
504
|
+
handle = CreateNamedPipe(PipePrefix + listener.Record.Token, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | (first ? FILE_FLAG_FIRST_PIPE_INSTANCE : 0), PIPE_TYPE_BYTE | PIPE_WAIT, MaxPipeInstances, MaxPipeBytes, MaxPipeBytes, PipeDeadlineMilliseconds, attributes);
|
|
505
|
+
if (handle == IntPtr.Zero || handle.ToInt64() == -1) { Close(handle); Fail("unavailable"); }
|
|
506
|
+
NamedPipeServerStream pipe = new NamedPipeServerStream(PipeDirection.InOut, true, false, new SafePipeHandle(handle, true)); handle = IntPtr.Zero;
|
|
507
|
+
try { AssertPipeOwned(pipe, listener.Sid); return pipe; } catch { pipe.Dispose(); throw; }
|
|
508
|
+
} finally { Close(handle); if (attributes != IntPtr.Zero) Marshal.FreeHGlobal(attributes); if (descriptor != IntPtr.Zero) Marshal.FreeHGlobal(descriptor); }
|
|
509
|
+
}
|
|
510
|
+
// Native failure notification has a fixed schema and is queued only after Gate-owned
|
|
511
|
+
// cleanup, so stdout backpressure cannot delay timer or handle release transitions.
|
|
512
|
+
static void ReportListenerFailure(int generation) {
|
|
513
|
+
ThreadPool.QueueUserWorkItem(delegate(object ignored) {
|
|
514
|
+
try { WriteControl("{\"event\":\"listener-failed\",\"generation\":" + generation.ToString(System.Globalization.CultureInfo.InvariantCulture) + ",\"error\":\"unavailable\"}"); } catch {}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
static void FailListener(OwnedListener listener) {
|
|
518
|
+
if (listener == null || listener.Stopped) return;
|
|
519
|
+
listener.Stopped = true;
|
|
520
|
+
// Remove the identity-checked publication while an owned pipe still holds
|
|
521
|
+
// the endpoint. An unsafe publication is left untouched rather than deleted.
|
|
522
|
+
try { RemoveListenerPublication(listener); } catch {}
|
|
523
|
+
foreach (PipeClient client in listener.Clients.ToArray()) ClosePipe(client);
|
|
524
|
+
if (Listener == listener) Listener = null;
|
|
525
|
+
ReportListenerFailure(listener.Generation);
|
|
526
|
+
}
|
|
527
|
+
static void ClosePipe(PipeClient client) {
|
|
528
|
+
if (client == null || client.Closed) return;
|
|
529
|
+
OwnedListener listener = client.Listener;
|
|
530
|
+
bool active = Listener == listener && !listener.Stopped;
|
|
531
|
+
// Clients owns every server instance, including pending accepts. Before
|
|
532
|
+
// disposing the final instance, establish its replacement while this
|
|
533
|
+
// owned handle still prevents a third party from claiming the endpoint.
|
|
534
|
+
if (active && listener.Clients.Count == 1 && !ArmAccept(listener, client)) { FailListener(listener); return; }
|
|
535
|
+
client.Closed = true;
|
|
536
|
+
try { if (client.Timer != null) client.Timer.Dispose(); } catch {} try { client.Pipe.Dispose(); } catch {} client.Listener.Clients.Remove(client);
|
|
537
|
+
if (active && !ArmAccept(listener, null)) FailListener(listener);
|
|
538
|
+
}
|
|
539
|
+
static void SendEvent(PipeClient client) {
|
|
540
|
+
string wire = Convert.ToBase64String(client.Bytes.ToArray());
|
|
541
|
+
lock (OutputGate) { Console.Out.WriteLine("{\"event\":\"notification\",\"connectionId\":\"" + client.ConnectionId + "\",\"generation\":" + client.Listener.Generation.ToString(System.Globalization.CultureInfo.InvariantCulture) + ",\"wire\":\"" + wire + "\"}"); }
|
|
542
|
+
}
|
|
543
|
+
static void FinishAck(IAsyncResult result) {
|
|
544
|
+
PipeClient client = (PipeClient)result.AsyncState;
|
|
545
|
+
try { client.Pipe.EndWrite(result); } catch {}
|
|
546
|
+
lock (Gate) { ClosePipe(client); }
|
|
547
|
+
}
|
|
548
|
+
// Gate owns state transitions; BeginWrite runs only after the transition unlocks.
|
|
549
|
+
static bool PrepareAckLocked(PipeClient client, out string id) {
|
|
550
|
+
id = null;
|
|
551
|
+
if (client == null || client.Closed || client.Writing || Listener != client.Listener || client.Listener.Stopped || String.IsNullOrEmpty(client.Id)) return false;
|
|
552
|
+
client.Writing = true;
|
|
553
|
+
try { client.Timer.Change(PipeDeadlineMilliseconds, Timeout.Infinite); }
|
|
554
|
+
catch { ClosePipe(client); return false; }
|
|
555
|
+
id = client.Id; return true;
|
|
556
|
+
}
|
|
557
|
+
static void BeginAck(PipeClient client, string id, bool accepted, string error) {
|
|
558
|
+
string value = accepted ? "{\"version\":1,\"kind\":\"ack\",\"id\":\"" + id + "\",\"accepted\":true}\n" : "{\"version\":1,\"kind\":\"ack\",\"id\":\"" + id + "\",\"accepted\":false,\"error\":\"" + error + "\"}\n";
|
|
559
|
+
try { byte[] bytes = new UTF8Encoding(false).GetBytes(value); client.Pipe.BeginWrite(bytes, 0, bytes.Length, FinishAck, client); }
|
|
560
|
+
catch { lock (Gate) { ClosePipe(client); } }
|
|
561
|
+
}
|
|
562
|
+
static void OnPipeTimeout(object state) {
|
|
563
|
+
PipeClient timed = (PipeClient)state; string id;
|
|
564
|
+
lock (Gate) {
|
|
565
|
+
if (timed.Closed || Listener != timed.Listener || timed.Listener.Stopped) return;
|
|
566
|
+
// The initial deadline closes an incomplete request; the rearmed deadline
|
|
567
|
+
// closes an ACK that has not completed, rather than silently returning.
|
|
568
|
+
if (String.IsNullOrEmpty(timed.Id) || timed.Writing) { ClosePipe(timed); return; }
|
|
569
|
+
if (!PrepareAckLocked(timed, out id)) return;
|
|
570
|
+
}
|
|
571
|
+
BeginAck(timed, id, false, "timeout");
|
|
572
|
+
}
|
|
573
|
+
static void ReadPipe(IAsyncResult result) {
|
|
574
|
+
PipeClient client = (PipeClient)result.AsyncState;
|
|
575
|
+
try { int count = client.Pipe.EndRead(result); lock (Gate) {
|
|
576
|
+
if (client.Closed || Listener != client.Listener || client.Listener.Stopped) return;
|
|
577
|
+
if (count <= 0 || client.Bytes.Length + count > MaxPipeBytes) { ClosePipe(client); return; }
|
|
578
|
+
client.Bytes.Write(client.Buffer, 0, count); byte[] bytes = client.Bytes.GetBuffer(); int length = (int)client.Bytes.Length, newline = Array.IndexOf(bytes, (byte)10, 0, length);
|
|
579
|
+
if (newline < 0) { client.Pipe.BeginRead(client.Buffer, 0, client.Buffer.Length, ReadPipe, client); return; }
|
|
580
|
+
if (newline != length - 1) { ClosePipe(client); return; }
|
|
581
|
+
string text; try { text = new UTF8Encoding(false, true).GetString(bytes, 0, newline); } catch { ClosePipe(client); return; }
|
|
582
|
+
Match match = Regex.Match(text, "^\\{\\s*\"version\"\\s*:\\s*1\\s*,\\s*\"kind\"\\s*:\\s*\"notification\"\\s*,\\s*\"id\"\\s*:\\s*\"([A-Za-z0-9][A-Za-z0-9_-]{0,127})\"");
|
|
583
|
+
if (!match.Success) { ClosePipe(client); return; } client.Id = match.Groups[1].Value; SendEvent(client);
|
|
584
|
+
}} catch { lock (Gate) { ClosePipe(client); } }
|
|
585
|
+
}
|
|
586
|
+
static bool ArmAccept(OwnedListener listener, PipeClient retiring = null) {
|
|
587
|
+
if (listener.Stopped || Listener != listener) return false;
|
|
588
|
+
foreach (PipeClient candidate in listener.Clients) if (candidate != retiring && !candidate.Closed && candidate.Accepting) return true;
|
|
589
|
+
if (listener.Clients.Count >= MaxPipeInstances) return true;
|
|
590
|
+
NamedPipeServerStream pipe = null; PipeClient client = null;
|
|
591
|
+
try { pipe = CreateOwnedPipe(listener, !listener.FirstCreated); listener.FirstCreated = true; client = new PipeClient(listener, pipe); client.Accepting = true; pipe = null; listener.Clients.Add(client); client.Pipe.BeginWaitForConnection(AcceptPipe, client); return true; }
|
|
592
|
+
catch {
|
|
593
|
+
if (pipe != null) pipe.Dispose();
|
|
594
|
+
if (client != null) { client.Closed = true; try { client.Pipe.Dispose(); } catch {} listener.Clients.Remove(client); }
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
static void AcceptPipe(IAsyncResult result) {
|
|
599
|
+
PipeClient client = (PipeClient)result.AsyncState;
|
|
600
|
+
try { client.Pipe.EndWaitForConnection(result); lock (Gate) {
|
|
601
|
+
if (client.Closed || Listener != client.Listener || client.Listener.Stopped) { ClosePipe(client); return; }
|
|
602
|
+
client.Accepting = false;
|
|
603
|
+
if (!ArmAccept(client.Listener)) { FailListener(client.Listener); return; }
|
|
604
|
+
client.Timer = new Timer(OnPipeTimeout, client, PipeDeadlineMilliseconds, Timeout.Infinite);
|
|
605
|
+
client.Pipe.BeginRead(client.Buffer, 0, client.Buffer.Length, ReadPipe, client);
|
|
606
|
+
}} catch { lock (Gate) { ClosePipe(client); } }
|
|
607
|
+
}
|
|
608
|
+
public static PresenceRecord Listen(string sessionId, long createdAt, int? requestedGeneration, byte[] pipeDescriptor, byte[] presenceDescriptor, string sid) {
|
|
609
|
+
lock (Gate) {
|
|
610
|
+
RequirePresence(); if (Listener != null) Fail("busy");
|
|
611
|
+
// Legacy four-field requests allocate helper epochs. New requests reserve
|
|
612
|
+
// a host epoch above the helper high-water mark; skipped local attempts
|
|
613
|
+
// therefore never force a reused or contiguous value after a late failure.
|
|
614
|
+
int generation;
|
|
615
|
+
if (requestedGeneration.HasValue) {
|
|
616
|
+
generation = requestedGeneration.Value;
|
|
617
|
+
if (generation < 1 || generation <= ListenerGeneration) Fail("invalid");
|
|
618
|
+
} else {
|
|
619
|
+
if (ListenerGeneration == Int32.MaxValue) Fail("invalid");
|
|
620
|
+
generation = ListenerGeneration + 1;
|
|
621
|
+
}
|
|
622
|
+
PresenceRecord record = NewRecord(sessionId, createdAt);
|
|
623
|
+
ListenerGeneration = generation;
|
|
624
|
+
OwnedListener listener = new OwnedListener(record, sid, pipeDescriptor, generation); Listener = listener;
|
|
625
|
+
try {
|
|
626
|
+
if (!ArmAccept(listener) || listener.Clients.Count == 0) Fail("unavailable");
|
|
627
|
+
// The returned record is published while Gate still protects readiness,
|
|
628
|
+
// so a later native failure removes this exact owned publication.
|
|
629
|
+
listener.Publication = PublishOwned(record.SessionId, record.Endpoint, record.CreatedAt, presenceDescriptor, sid);
|
|
630
|
+
return record;
|
|
631
|
+
} catch { FailListener(listener); throw; }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
public static void Acknowledge(string connectionId, int generation, string id, bool accepted) {
|
|
635
|
+
PipeClient selected = null; string ackId;
|
|
636
|
+
lock (Gate) {
|
|
637
|
+
if (Listener == null || Listener.Stopped || Listener.Generation != generation) return;
|
|
638
|
+
foreach (PipeClient client in Listener.Clients) if (!client.Closed && client.ConnectionId == connectionId && client.Id == id) { selected = client; break; }
|
|
639
|
+
if (selected == null || !PrepareAckLocked(selected, out ackId)) return;
|
|
640
|
+
}
|
|
641
|
+
BeginAck(selected, ackId, accepted, accepted ? null : "rejected");
|
|
642
|
+
}
|
|
643
|
+
public static void StopListener(string sessionId, string endpoint, long createdAt, string sid) {
|
|
644
|
+
lock (Gate) { if (Listener == null) return; string token; ValidateRecord(sessionId, endpoint, createdAt, out token); if (!SameRecord(Listener.Record, sessionId, endpoint, createdAt) || Listener.Sid != sid) Fail("unsafe"); OwnedListener listener = Listener; listener.Stopped = true; try { RemoveListenerPublication(listener); } finally { foreach (PipeClient client in listener.Clients.ToArray()) ClosePipe(client); Listener = null; } }
|
|
645
|
+
}
|
|
646
|
+
public static void Initialize(string agentHome, byte[] descriptor, string sid) {
|
|
647
|
+
lock (Gate) {
|
|
648
|
+
InitializationStage = "unknown"; InitializationNtStatus = null;
|
|
649
|
+
CloseAll();
|
|
650
|
+
try {
|
|
651
|
+
string[] components = Components(agentHome);
|
|
652
|
+
uint volumeSerial;
|
|
653
|
+
SetStage("volume-open"); IntPtr volume = RequireVolumeOpen(VolumePath(agentHome, out volumeSerial), volumeSerial); Handles.Add(volume); IntPtr parent = volume;
|
|
654
|
+
foreach (string component in components) { SetStage("ancestor-open"); IntPtr child = RequireOpen(parent, component, false); Handles.Add(child); parent = child; }
|
|
655
|
+
// gentle-agents is a shared routing parent created by the host lifecycle; never repair or create it here.
|
|
656
|
+
SetStage("routing-open"); IntPtr routing = RequireOpen(parent, "gentle-agents", false); Handles.Add(routing);
|
|
657
|
+
IntPtr transport = CreateOrOpenOwned(routing, "transport", descriptor, sid, "transport-create-or-open", "transport-assert-owned"); Handles.Add(transport);
|
|
658
|
+
Presence = CreateOrOpenOwned(transport, "presence", descriptor, sid, "presence-create-or-open", "presence-assert-owned"); Handles.Add(Presence);
|
|
659
|
+
} catch { CloseAll(); throw; } finally { InitializationStage = "unknown"; InitializationNtStatus = null; }
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
public static void CloseAll() {
|
|
663
|
+
lock (Gate) {
|
|
664
|
+
// Stop accepts and clients before removing owned presence and ancestor handles.
|
|
665
|
+
if (Listener != null) { try { StopListener(Listener.Record.SessionId, Listener.Record.Endpoint, Listener.Record.CreatedAt, Listener.Sid); } catch { Listener = null; } }
|
|
666
|
+
// Cleanup is best effort but always releases every retained publication handle before ancestors close.
|
|
667
|
+
for (int index = OwnedPublications.Count - 1; index >= 0; index--) { OwnedPublication owned = OwnedPublications[index]; try { RemoveOwnedPublication(owned); } catch { if (OwnedPublications.Contains(owned)) OwnedPublications.Remove(owned); owned.Dispose(); } }
|
|
668
|
+
for (int index = Handles.Count - 1; index >= 0; index--) Close(Handles[index]);
|
|
669
|
+
Handles.Clear(); OwnedPublications.Clear(); Presence = IntPtr.Zero;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
'@
|
|
674
|
+
$nativeReady = $true
|
|
675
|
+
} catch {
|
|
676
|
+
$nativeReady = $false
|
|
677
|
+
Write-BootstrapDiagnostic (@($addTypeErrors) + @($_))
|
|
678
|
+
}
|
|
679
|
+
if ($nativeReady) {
|
|
680
|
+
[WindowsSessionBootstrap]::WriteControl('{"event":"startup-marker","marker":"native-ready"}')
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function Write-BootstrapRejectionDiagnostic([BootstrapFailure]$failure) {
|
|
684
|
+
if ($failure.Code -ne 'unsafe') { return }
|
|
685
|
+
$stage = 'unknown'
|
|
686
|
+
$ntstatus = $null
|
|
687
|
+
try {
|
|
688
|
+
$candidateStage = Get-BootstrapProperty $failure 'Stage'
|
|
689
|
+
if ($candidateStage -is [string] -and $candidateStage -in @('volume-open', 'ancestor-open', 'routing-open', 'transport-create-or-open', 'transport-assert-owned', 'presence-create-or-open', 'presence-assert-owned')) { $stage = $candidateStage }
|
|
690
|
+
$candidateStatus = Get-BootstrapProperty $failure 'NtStatus'
|
|
691
|
+
if ($candidateStatus -is [uint32]) { $ntstatus = [uint64]$candidateStatus }
|
|
692
|
+
} catch {}
|
|
693
|
+
[Console]::Error.WriteLine(([pscustomobject]@{ kind = 'windows-session-bootstrap-rejection'; stage = $stage; ntstatus = $ntstatus } | ConvertTo-Json -Compress))
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function Write-Reply([string]$requestId, [bool]$ok, $result, [string]$error) {
|
|
697
|
+
$line = if ($ok) { @{ requestId = $requestId; ok = $true; result = $result } | ConvertTo-Json -Compress -Depth 4 } else { @{ requestId = $requestId; ok = $false; error = $error } | ConvertTo-Json -Compress }
|
|
698
|
+
if ($nativeReady) { [WindowsSessionBootstrap]::WriteControl($line) } else { [Console]::Out.WriteLine($line) }
|
|
699
|
+
}
|
|
700
|
+
function Get-CurrentPrivateDescriptor {
|
|
701
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
702
|
+
$security = [Security.AccessControl.DirectorySecurity]::new(); $security.SetAccessRuleProtection($true, $false); $security.SetOwner($sid)
|
|
703
|
+
$security.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($sid, [Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow))
|
|
704
|
+
return [pscustomobject]@{ Sid = $sid.Value; Descriptor = $security.GetSecurityDescriptorBinaryForm() }
|
|
705
|
+
}
|
|
706
|
+
function Get-CurrentPipeDescriptor {
|
|
707
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
708
|
+
$security = [IO.Pipes.PipeSecurity]::new(); $security.SetAccessRuleProtection($true, $false); $security.SetOwner($sid)
|
|
709
|
+
$security.AddAccessRule([IO.Pipes.PipeAccessRule]::new($sid, [IO.Pipes.PipeAccessRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow))
|
|
710
|
+
return [pscustomobject]@{ Sid = $sid.Value; Descriptor = $security.GetSecurityDescriptorBinaryForm() }
|
|
711
|
+
}
|
|
712
|
+
function ConvertTo-PublicPresenceRecord($record) {
|
|
713
|
+
return [pscustomobject]@{ version = 1; sessionId = $record.SessionId; endpoint = $record.Endpoint; createdAt = $record.CreatedAt }
|
|
714
|
+
}
|
|
715
|
+
function Try-PresenceCreatedAt([object]$value, [ref]$createdAt) {
|
|
716
|
+
$candidate = [int64]0
|
|
717
|
+
if ($value -is [sbyte] -or $value -is [byte] -or $value -is [int16] -or $value -is [uint16] -or $value -is [int32] -or $value -is [uint32] -or $value -is [int64]) { $candidate = [int64]$value }
|
|
718
|
+
elseif ($value -is [uint64]) { if ($value -gt [uint64]9007199254740991) { return $false }; $candidate = [int64]$value }
|
|
719
|
+
else { return $false }
|
|
720
|
+
if ($candidate -lt 0 -or $candidate -gt 9007199254740991) { return $false }
|
|
721
|
+
$createdAt.Value = $candidate
|
|
722
|
+
return $true
|
|
723
|
+
}
|
|
724
|
+
function Is-PresenceRecord($record, [ref]$createdAt) {
|
|
725
|
+
if ($null -eq $record -or $record -isnot [psobject]) { return $false }
|
|
726
|
+
$names = @($record.PSObject.Properties | ForEach-Object { $_.Name })
|
|
727
|
+
if ($names.Count -ne 4 -or @($names | Where-Object { $_ -notin @('version', 'sessionId', 'endpoint', 'createdAt') }).Count -ne 0) { return $false }
|
|
728
|
+
return $record.version -eq 1 -and $record.sessionId -is [string] -and $record.endpoint -is [string] -and (Try-PresenceCreatedAt $record.createdAt $createdAt)
|
|
729
|
+
}
|
|
730
|
+
function Is-RequestId([object]$value) { return $value -is [string] -and $value -match '^[A-Za-z0-9-]{1,128}$' }
|
|
731
|
+
function Is-ExactRequest($request, [string[]]$names) {
|
|
732
|
+
$actual = @($request.PSObject.Properties | ForEach-Object { $_.Name })
|
|
733
|
+
return $actual.Count -eq $names.Count -and @($actual | Where-Object { $_ -notin $names }).Count -eq 0
|
|
734
|
+
}
|
|
735
|
+
function Read-ControlLine {
|
|
736
|
+
$buffer = [Text.StringBuilder]::new()
|
|
737
|
+
$bytes = 0
|
|
738
|
+
$overflow = $false
|
|
739
|
+
$next = -1
|
|
740
|
+
while (($next = [Console]::In.Read()) -ne -1) {
|
|
741
|
+
if ($next -eq 10) { break }
|
|
742
|
+
if ($next -eq 13) { continue }
|
|
743
|
+
$char = [char]$next
|
|
744
|
+
$bytes += [Text.Encoding]::UTF8.GetByteCount([string]$char)
|
|
745
|
+
if ($bytes -gt $maxControlBytes) { $overflow = $true; continue }
|
|
746
|
+
[void]$buffer.Append($char)
|
|
747
|
+
}
|
|
748
|
+
if ($next -eq -1 -and $buffer.Length -eq 0 -and -not $overflow) { return $null }
|
|
749
|
+
if ($overflow) { return '' }
|
|
750
|
+
return $buffer.ToString()
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
:requests while (($line = Read-ControlLine) -ne $null) {
|
|
755
|
+
if ([Text.Encoding]::UTF8.GetByteCount($line) -gt $maxControlBytes) { break }
|
|
756
|
+
$request = $null
|
|
757
|
+
try { $request = $line | ConvertFrom-Json -ErrorAction Stop } catch { break }
|
|
758
|
+
if ($null -eq $request -or -not (Is-RequestId $request.requestId) -or $request.operation -isnot [string]) { break }
|
|
759
|
+
$id = $request.requestId
|
|
760
|
+
switch ($request.operation) {
|
|
761
|
+
'start' {
|
|
762
|
+
if (-not (Is-ExactRequest $request @('requestId', 'operation'))) { Write-Reply $id $false $null 'invalid'; break requests }
|
|
763
|
+
if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }
|
|
764
|
+
Write-Reply $id $true @{ state = 'partial' } $null; break
|
|
765
|
+
}
|
|
766
|
+
'initialize' {
|
|
767
|
+
if (-not (Is-ExactRequest $request @('requestId', 'operation', 'agentHome')) -or $request.agentHome -isnot [string]) { Write-Reply $id $false $null 'invalid'; break requests }
|
|
768
|
+
if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }
|
|
769
|
+
try {
|
|
770
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
771
|
+
$security = [Security.AccessControl.DirectorySecurity]::new(); $security.SetAccessRuleProtection($true, $false); $security.SetOwner($sid)
|
|
772
|
+
$security.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($sid, [Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.AccessControlType]::Allow))
|
|
773
|
+
[WindowsSessionBootstrap]::Initialize($request.agentHome, $security.GetSecurityDescriptorBinaryForm(), $sid.Value)
|
|
774
|
+
Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete' } $null
|
|
775
|
+
} catch [BootstrapFailure] { Write-BootstrapRejectionDiagnostic $_.Exception; Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }
|
|
776
|
+
break
|
|
777
|
+
}
|
|
778
|
+
'enumerate' { if (-not (Is-ExactRequest $request @('requestId', 'operation'))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete'; entries = [WindowsSessionBootstrap]::EnumeratePresence() } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
779
|
+
'record' { $createdAt = [int64]0; if (-not (Is-ExactRequest $request @('requestId', 'operation', 'sessionId', 'createdAt')) -or $request.sessionId -isnot [string] -or -not (Try-PresenceCreatedAt $request.createdAt ([ref]$createdAt))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { Write-Reply $id $true (ConvertTo-PublicPresenceRecord ([WindowsSessionBootstrap]::NewRecord($request.sessionId, $createdAt))) $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
780
|
+
'publish' { $createdAt = [int64]0; if (-not (Is-ExactRequest $request @('requestId', 'operation', 'record')) -or -not (Is-PresenceRecord $request.record ([ref]$createdAt))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $identity = Get-CurrentPrivateDescriptor; [WindowsSessionBootstrap]::Publish($request.record.sessionId, $request.record.endpoint, $createdAt, $identity.Descriptor, $identity.Sid); Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete' } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
781
|
+
'list' { $hasExcludeSessionId = Is-ExactRequest $request @('requestId', 'operation', 'excludeSessionId'); if ($hasExcludeSessionId) { $excludeSessionId = $request.excludeSessionId; if ($excludeSessionId -isnot [string]) { Write-Reply $id $false $null 'invalid'; break requests } } elseif (-not (Is-ExactRequest $request @('requestId', 'operation'))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $identity = Get-CurrentPrivateDescriptor; $nativeRecords = if ($hasExcludeSessionId) { [WindowsSessionBootstrap]::List($excludeSessionId, $identity.Sid) } else { [WindowsSessionBootstrap]::List($identity.Sid) }; $records = @($nativeRecords | ForEach-Object { ConvertTo-PublicPresenceRecord $_ }); Write-Reply $id $true @{ records = $records } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
782
|
+
'resolve' { if (-not (Is-ExactRequest $request @('requestId', 'operation', 'sessionId')) -or $request.sessionId -isnot [string]) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $identity = Get-CurrentPrivateDescriptor; Write-Reply $id $true (ConvertTo-PublicPresenceRecord ([WindowsSessionBootstrap]::Resolve($request.sessionId, $identity.Sid))) $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
783
|
+
'remove' { $createdAt = [int64]0; if (-not (Is-ExactRequest $request @('requestId', 'operation', 'record')) -or -not (Is-PresenceRecord $request.record ([ref]$createdAt))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $identity = Get-CurrentPrivateDescriptor; [WindowsSessionBootstrap]::RemoveOwn($request.record.sessionId, $request.record.endpoint, $createdAt, $identity.Sid); Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete' } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
784
|
+
'listen' { $createdAt = [int64]0; $hasGeneration = Is-ExactRequest $request @('requestId', 'operation', 'sessionId', 'createdAt', 'generation'); $legacy = Is-ExactRequest $request @('requestId', 'operation', 'sessionId', 'createdAt'); if ((-not $hasGeneration -and -not $legacy) -or $request.sessionId -isnot [string] -or -not (Try-PresenceCreatedAt $request.createdAt ([ref]$createdAt))) { Write-Reply $id $false $null 'invalid'; break requests }; $requestedGeneration = $null; if ($hasGeneration) { $generation = [int64]0; if (-not (Try-PresenceCreatedAt $request.generation ([ref]$generation)) -or $generation -lt 1 -or $generation -gt 2147483647) { Write-Reply $id $false $null 'invalid'; break requests }; $requestedGeneration = [Nullable[int]]([int]$generation) }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $pipeIdentity = Get-CurrentPipeDescriptor; $presenceIdentity = Get-CurrentPrivateDescriptor; if ($pipeIdentity.Sid -ne $presenceIdentity.Sid) { throw [System.InvalidOperationException]::new() }; Write-Reply $id $true (ConvertTo-PublicPresenceRecord ([WindowsSessionBootstrap]::Listen($request.sessionId, $createdAt, $requestedGeneration, $pipeIdentity.Descriptor, $presenceIdentity.Descriptor, $pipeIdentity.Sid))) $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
785
|
+
'ack' { $generation = [int64]0; if (-not (Is-ExactRequest $request @('requestId', 'operation', 'connectionId', 'generation', 'id', 'accepted')) -or -not (Is-RequestId $request.connectionId) -or -not (Is-RequestId $request.id) -or -not (Try-PresenceCreatedAt $request.generation ([ref]$generation)) -or $generation -lt 1 -or $generation -gt 2147483647 -or $request.accepted -isnot [bool]) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { [WindowsSessionBootstrap]::Acknowledge($request.connectionId, [int]$generation, $request.id, $request.accepted); Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete' } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
786
|
+
'stop-listener' { $createdAt = [int64]0; if (-not (Is-ExactRequest $request @('requestId', 'operation', 'record')) -or -not (Is-PresenceRecord $request.record ([ref]$createdAt))) { Write-Reply $id $false $null 'invalid'; break requests }; if (-not $nativeReady) { Write-Reply $id $false $null 'unavailable'; break }; try { $identity = Get-CurrentPrivateDescriptor; [WindowsSessionBootstrap]::StopListener($request.record.sessionId, $request.record.endpoint, $createdAt, $identity.Sid); Write-Reply $id $true @{ state = 'initialized'; bootstrap = 'complete' } $null } catch [BootstrapFailure] { Write-Reply $id $false $null $_.Exception.Code } catch { Write-Reply $id $false $null 'unavailable' }; break }
|
|
787
|
+
'shutdown' { if (-not (Is-ExactRequest $request @('requestId', 'operation'))) { Write-Reply $id $false $null 'invalid'; break requests }; if ($nativeReady) { [WindowsSessionBootstrap]::CloseAll() }; Write-Reply $id $true @{ state = 'partial' } $null; break }
|
|
788
|
+
default { Write-Reply $id $false $null 'invalid'; break }
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
} finally { if ($nativeReady) { [WindowsSessionBootstrap]::CloseAll() } }
|