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.
- package/CHANGELOG.md +171 -1
- package/CONTRIBUTING.md +46 -13
- package/README.md +109 -524
- package/THIRD_PARTY_NOTICES.txt +1 -28
- package/autostart/windows/account-task.ps1 +167 -0
- package/autostart/windows/inspect-task.ps1 +42 -0
- package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
- package/autostart/windows/legacy-access.ps1 +359 -0
- package/autostart/windows/legacy-task.ps1 +301 -0
- package/autostart/windows/register-task.ps1 +11 -2
- package/bin/account-task-adapter.mjs +47 -0
- package/bin/account-upgrade-controller.mjs +198 -0
- package/bin/account-upgrade-worker.mjs +141 -0
- package/bin/account-worker-session.mjs +215 -0
- package/bin/cli-dispatch.mjs +154 -0
- package/bin/cli-main.mjs +1063 -0
- package/bin/cli-selection.mjs +43 -0
- package/bin/cli.mjs +7 -876
- package/bin/dependency-lock.json +642 -0
- package/bin/legacy-1.3.json +32 -0
- package/bin/legacy-access.mjs +73 -0
- package/bin/legacy-installation.mjs +273 -0
- package/bin/legacy-task.mjs +76 -0
- package/bin/legacy-upgrade.mjs +527 -0
- package/bin/managed-runtime.mjs +170 -0
- package/bin/managed-state.mjs +257 -0
- package/bin/managed-upgrade.mjs +581 -0
- package/bin/mcp-bridge.mjs +150 -4
- package/bin/pooling-batches.mjs +13 -0
- package/bin/pooling-writer.mjs +4 -0
- package/bin/prewarm.mjs +59 -25
- package/bin/runtime-context.mjs +100 -0
- package/bin/service-control.mjs +32 -4
- package/bin/stable-launcher.mjs +54 -0
- package/bin/task-only-controller.mjs +226 -0
- package/bin/task-scope.mjs +65 -0
- package/bin/task-transaction-protocol.mjs +173 -0
- package/bin/top.mjs +22 -10
- package/bin/update-check.mjs +13 -2
- package/bin/upgrade-autostart.mjs +47 -0
- package/bin/upgrade-capability.json +9 -0
- package/bin/upgrade-dependencies.mjs +183 -0
- package/bin/upgrade-package.mjs +205 -0
- package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
- package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
- package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
- package/bin/windows-legacy/src/LegacyNative.cs +832 -0
- package/bin/windows-legacy/src/LegacyProcessBroker.cs +961 -0
- package/bin/windows-legacy-process.mjs +248 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
- package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
- package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
- package/bin/windows-process-lifetime.mjs +218 -0
- package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
- package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
- package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
- package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
- package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
- package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
- package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
- package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
- package/bin/windows-task-channel.mjs +335 -0
- package/docs/account-upgrades.md +218 -0
- package/docs/compatibility.md +39 -0
- package/docs/configuration.md +146 -0
- package/docs/dashboards.md +125 -0
- package/docs/execution-context.md +119 -0
- package/docs/http-api.md +47 -0
- package/docs/install.md +52 -0
- package/docs/operations.md +99 -0
- package/docs/upgrades.md +324 -0
- package/package.json +19 -5
- package/supervisor/supervise.mjs +226 -40
- package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
- package/tools/windows-legacy/README.md +251 -0
- package/tools/windows-legacy/build.ps1 +122 -0
- package/tools/windows-legacy/inventory.mjs +92 -0
- package/tools/windows-process-lifetime/README.md +119 -0
- package/tools/windows-process-lifetime/build.ps1 +122 -0
- package/tools/windows-process-lifetime/inventory.mjs +86 -0
- package/tools/windows-task-channel/README.md +329 -0
- package/tools/windows-task-channel/build.ps1 +107 -0
- package/tools/windows-task-channel/inventory.mjs +117 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
- package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
- package/ui/dist/assets/index-PVcuDNrN.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/third-party-manifest.json +37 -25
- package/ui/dist/assets/index-CaLPg0c-.css +0 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Builds or verifies the separate Windows legacy observed-set broker.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Uses installed Visual Studio Roslyn and .NET Framework 4.6.2 reference assemblies.
|
|
6
|
+
Compiles twice under different paths, records every input, and downloads nothing.
|
|
7
|
+
#>
|
|
8
|
+
[CmdletBinding()]
|
|
9
|
+
param(
|
|
10
|
+
[switch]$Verify,
|
|
11
|
+
[string]$CompilerPath,
|
|
12
|
+
[string]$ReferenceAssemblyPath
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Set-StrictMode -Version Latest
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
|
|
18
|
+
function Get-Digest([string]$Path, [switch]$Text) {
|
|
19
|
+
$bytes = [IO.File]::ReadAllBytes($Path)
|
|
20
|
+
if ($Text) {
|
|
21
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes([IO.File]::ReadAllText($Path).Replace("`r`n", "`n"))
|
|
22
|
+
}
|
|
23
|
+
$hash = [Security.Cryptography.SHA256]::Create()
|
|
24
|
+
try { return [BitConverter]::ToString($hash.ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() }
|
|
25
|
+
finally { $hash.Dispose() }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if ($env:OS -ne 'Windows_NT') { throw 'Build on Windows with installed Roslyn and .NET Framework 4.6.2 references.' }
|
|
29
|
+
$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
|
30
|
+
$directory = Join-Path $root 'bin\windows-legacy'
|
|
31
|
+
$binary = 'LegacyProcessBroker.exe'
|
|
32
|
+
$metadataPath = Join-Path $directory 'LegacyProcessBroker.build.json'
|
|
33
|
+
$sources = @('AssemblyInfo.cs', 'LegacyNative.cs', 'LegacyProcessBroker.cs')
|
|
34
|
+
$references = @('mscorlib.dll', 'System.dll', 'System.Core.dll', 'System.Management.dll', 'System.Web.Extensions.dll')
|
|
35
|
+
if (-not $CompilerPath) {
|
|
36
|
+
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
|
37
|
+
if (-not (Test-Path -LiteralPath $vswhere)) { throw 'Specify an installed Roslyn -CompilerPath; no tools are installed.' }
|
|
38
|
+
$installation = & $vswhere -latest -products '*' -requires Microsoft.Component.MSBuild -property installationPath
|
|
39
|
+
if ($LASTEXITCODE -ne 0 -or
|
|
40
|
+
-not $installation) { throw 'No installed Visual Studio MSBuild compiler found.' }
|
|
41
|
+
$CompilerPath = Join-Path $installation 'MSBuild\Current\Bin\Roslyn\csc.exe'
|
|
42
|
+
}
|
|
43
|
+
if (-not $ReferenceAssemblyPath) {
|
|
44
|
+
$ReferenceAssemblyPath = Join-Path ${env:ProgramFiles(x86)} 'Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2'
|
|
45
|
+
}
|
|
46
|
+
$CompilerPath = (Resolve-Path -LiteralPath $CompilerPath).Path
|
|
47
|
+
$ReferenceAssemblyPath = (Resolve-Path -LiteralPath $ReferenceAssemblyPath).Path
|
|
48
|
+
$framework = [xml][IO.File]::ReadAllText((Join-Path $ReferenceAssemblyPath 'RedistList\FrameworkList.xml'))
|
|
49
|
+
if ($framework.FileList.Name -cne '.NET Framework 4.6.2') { throw 'Wrong reference assembly targeting pack.' }
|
|
50
|
+
$version = (& $CompilerPath /version | Out-String).Trim()
|
|
51
|
+
if ($LASTEXITCODE -ne 0) { throw 'Roslyn version probe failed.' }
|
|
52
|
+
$options = @('/nologo', '/noconfig', '/nostdlib+', '/target:exe', '/platform:anycpu',
|
|
53
|
+
'/langversion:7.3', '/optimize+', '/debug-', '/deterministic+', '/checked-',
|
|
54
|
+
'/unsafe-', '/warn:4', '/warnaserror+', '/utf8output')
|
|
55
|
+
$sourceHashes = [ordered]@{}
|
|
56
|
+
foreach ($name in $sources) { $sourceHashes[$name] = Get-Digest (Join-Path $directory "src\$name") -Text }
|
|
57
|
+
$referenceHashes = [ordered]@{}
|
|
58
|
+
foreach ($name in $references) { $referenceHashes[$name] = Get-Digest (Join-Path $ReferenceAssemblyPath $name) }
|
|
59
|
+
$inputs = [ordered]@{
|
|
60
|
+
targetFramework = '.NETFramework,Version=v4.6.2'
|
|
61
|
+
platform = 'AnyCPU'
|
|
62
|
+
compilerVersion = $version
|
|
63
|
+
compilerSha256 = Get-Digest $CompilerPath
|
|
64
|
+
compilerArguments = $options
|
|
65
|
+
pathMap = '/_/mcp-pacemaker/windows-legacy'
|
|
66
|
+
sourceEncoding = 'UTF-8 without BOM, LF line endings'
|
|
67
|
+
sourceSha256 = $sourceHashes
|
|
68
|
+
buildScriptSha256 = Get-Digest $PSCommandPath -Text
|
|
69
|
+
referenceSha256 = $referenceHashes
|
|
70
|
+
}
|
|
71
|
+
$expected = $null
|
|
72
|
+
if ($Verify) {
|
|
73
|
+
$expected = [IO.File]::ReadAllText($metadataPath) | ConvertFrom-Json
|
|
74
|
+
if ($expected.schemaVersion -ne 1 -or
|
|
75
|
+
$expected.binary -cne $binary -or
|
|
76
|
+
(ConvertTo-Json $inputs -Depth 6 -Compress) -cne (ConvertTo-Json $expected.inputs -Depth 6 -Compress) -or
|
|
77
|
+
(Get-Digest (Join-Path $directory $binary)) -cne $expected.binarySha256) {
|
|
78
|
+
throw 'Packaged legacy broker inputs/toolchain/bytes do not match metadata. Verification changes nothing.'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
$temporary = Join-Path ([IO.Path]::GetTempPath()) ('mcp-legacy-build-' + [Guid]::NewGuid().ToString('N'))
|
|
82
|
+
$files = [Collections.Generic.List[string]]::new()
|
|
83
|
+
$directories = [Collections.Generic.List[string]]::new()
|
|
84
|
+
$outputs = [Collections.Generic.List[string]]::new()
|
|
85
|
+
try {
|
|
86
|
+
$null = [IO.Directory]::CreateDirectory($temporary)
|
|
87
|
+
$directories.Add($temporary)
|
|
88
|
+
foreach ($pass in @('first', 'second')) {
|
|
89
|
+
$scratch = Join-Path $temporary $pass
|
|
90
|
+
$null = [IO.Directory]::CreateDirectory($scratch)
|
|
91
|
+
$directories.Add($scratch)
|
|
92
|
+
$paths = foreach ($name in $sources) {
|
|
93
|
+
$path = Join-Path $scratch $name
|
|
94
|
+
$files.Add($path)
|
|
95
|
+
$text = [IO.File]::ReadAllText((Join-Path $directory "src\$name")).Replace("`r`n", "`n")
|
|
96
|
+
[IO.File]::WriteAllText($path, $text, [Text.UTF8Encoding]::new($false))
|
|
97
|
+
$path
|
|
98
|
+
}
|
|
99
|
+
$output = Join-Path $scratch $binary
|
|
100
|
+
$files.Add($output)
|
|
101
|
+
$arguments = $options + @("/out:$output", "/pathmap:$scratch=$($inputs.pathMap)")
|
|
102
|
+
foreach ($name in $references) { $arguments += '/reference:' + (Join-Path $ReferenceAssemblyPath $name) }
|
|
103
|
+
& $CompilerPath @arguments @paths
|
|
104
|
+
if ($LASTEXITCODE -ne 0) { throw "Legacy broker compile failed: $pass" }
|
|
105
|
+
$outputs.Add($output)
|
|
106
|
+
}
|
|
107
|
+
$digest = Get-Digest $outputs[0]
|
|
108
|
+
if ($digest -cne (Get-Digest $outputs[1])) { throw 'Two builds differ; no package assets changed.' }
|
|
109
|
+
if ($Verify) {
|
|
110
|
+
if ($digest -cne $expected.binarySha256) { throw 'Rebuilt bytes differ; no package assets changed.' }
|
|
111
|
+
Write-Output "Verified legacy broker source, toolchain and byte-for-byte rebuild: SHA256 $digest"
|
|
112
|
+
} else {
|
|
113
|
+
$metadata = [ordered]@{ schemaVersion = 1; binary = $binary; binarySha256 = $digest; inputs = $inputs }
|
|
114
|
+
[IO.File]::Copy($outputs[0], (Join-Path $directory $binary), $true)
|
|
115
|
+
[IO.File]::WriteAllText($metadataPath,
|
|
116
|
+
(ConvertTo-Json $metadata -Depth 8).Replace("`r`n", "`n") + "`n", [Text.UTF8Encoding]::new($false))
|
|
117
|
+
Write-Output "Built two matching legacy brokers with Roslyn ${version}: SHA256 $digest"
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
foreach ($path in $files) { Remove-Item -LiteralPath $path -ErrorAction SilentlyContinue }
|
|
121
|
+
for ($index = $directories.Count - 1; $index -ge 0; $index--) { [IO.Directory]::Delete($directories[$index]) }
|
|
122
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const LEGACY_BROKER_FILES = Object.freeze([
|
|
7
|
+
'bin/windows-legacy-process.mjs',
|
|
8
|
+
'bin/windows-legacy/LegacyProcessBroker.exe',
|
|
9
|
+
'bin/windows-legacy/LegacyProcessBroker.build.json',
|
|
10
|
+
'bin/windows-legacy/src/AssemblyInfo.cs',
|
|
11
|
+
'bin/windows-legacy/src/LegacyNative.cs',
|
|
12
|
+
'bin/windows-legacy/src/LegacyProcessBroker.cs',
|
|
13
|
+
'tools/windows-legacy/build.ps1',
|
|
14
|
+
'tools/windows-legacy/inventory.mjs',
|
|
15
|
+
'tools/windows-legacy/README.md',
|
|
16
|
+
]);
|
|
17
|
+
export const LEGACY_BROKER_TEST = 'test/windows-legacy-process.test.mjs';
|
|
18
|
+
export const LEGACY_BROKER_REQUIRED_TESTS = Object.freeze([
|
|
19
|
+
'legacy API denials preserve refusal and bounded stored-only failure diagnostics',
|
|
20
|
+
'legacy failure metadata cannot mask errors or cross concurrent operation identities',
|
|
21
|
+
'legacy stop sweep stops descendants of held parents and skips reused-PID edges',
|
|
22
|
+
'legacy protected-plan model enforces 512 combined identities and UTF8 byte bounds without process queries',
|
|
23
|
+
'legacy expected request refuses oversized UTF8 framing before native discovery',
|
|
24
|
+
...['relative', 'drive-relative', 'root-relative'].flatMap((spelling) =>
|
|
25
|
+
['bridge', 'supervisor'].map((affected) =>
|
|
26
|
+
`legacy script identity rejects ${affected} ${spelling} argv without stopping targets`)),
|
|
27
|
+
'legacy script identity accepts fully qualified Unicode and space paths',
|
|
28
|
+
'legacy script qualification recognizes drive and UNC forms without cwd resolution',
|
|
29
|
+
'legacy script identity preserves fully qualified short-path handling',
|
|
30
|
+
'legacy script identity does not equate a junction spelling with a different canonical root',
|
|
31
|
+
'legacy broker plans only its roots and stops the processes they started',
|
|
32
|
+
'legacy broker stops a server the bridge ran through cmd.exe, which Node ends with the bridge',
|
|
33
|
+
'legacy broker refuses an upgrade started from inside the 1.3 tree without stopping anything',
|
|
34
|
+
'legacy broker close is read-only and an identical expected plan can be reopened',
|
|
35
|
+
'legacy broker reopens its plan while sessions start and end, and stops the later ones',
|
|
36
|
+
'legacy broker rejects a stale root generation without stopping roots',
|
|
37
|
+
'legacy broker rejects wrong root and malformed protected verification plan',
|
|
38
|
+
'legacy broker refuses extra supervisor argv and a foreign listener without process mutation',
|
|
39
|
+
'legacy read-only recovery does not stop an unrelated process reusing the endpoint',
|
|
40
|
+
'legacy native inventory rejects changed executable, source and build metadata',
|
|
41
|
+
'legacy prepare and recovery verify refuse wrong-hash packaged helper before any discovery',
|
|
42
|
+
'legacy broker parent loss releases read-only capture without stopping legacy roots',
|
|
43
|
+
'legacy broker reports a session process it could not stop and still verifies the root stop',
|
|
44
|
+
'legacy broker preserves Unicode plan identity across fragmented UTF8 pipe messages',
|
|
45
|
+
'legacy broker sweep leaves an older process alive when it names a stopped root as parent',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function verifyLegacyBrokerAssets(root) {
|
|
49
|
+
for (const path of ['bin', 'bin/windows-legacy', 'bin/windows-legacy/src',
|
|
50
|
+
'tools', 'tools/windows-legacy']) {
|
|
51
|
+
const stat = lstatSync(join(root, path));
|
|
52
|
+
assert.ok(stat.isDirectory() &&
|
|
53
|
+
!stat.isSymbolicLink(), `Linked or invalid legacy broker directory: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
56
|
+
const files = LEGACY_BROKER_FILES.map((path) => {
|
|
57
|
+
const full = join(root, path);
|
|
58
|
+
const stat = lstatSync(full);
|
|
59
|
+
assert.ok(stat.isFile() &&
|
|
60
|
+
!stat.isSymbolicLink() &&
|
|
61
|
+
stat.size > 0, `Invalid legacy broker asset: ${path}`);
|
|
62
|
+
return { path, sha256: hash(readFileSync(full)) };
|
|
63
|
+
});
|
|
64
|
+
assert.deepEqual(readdirSync(join(root, 'bin/windows-legacy')).sort(), [
|
|
65
|
+
'LegacyProcessBroker.build.json', 'LegacyProcessBroker.exe', 'src',
|
|
66
|
+
]);
|
|
67
|
+
const sources = ['AssemblyInfo.cs', 'LegacyNative.cs', 'LegacyProcessBroker.cs'];
|
|
68
|
+
assert.deepEqual(readdirSync(join(root, 'bin/windows-legacy/src')).sort(), sources);
|
|
69
|
+
const metadata = JSON.parse(readFileSync(join(root, 'bin/windows-legacy/LegacyProcessBroker.build.json')));
|
|
70
|
+
assert.equal(metadata.schemaVersion, 1);
|
|
71
|
+
assert.equal(metadata.binary, 'LegacyProcessBroker.exe');
|
|
72
|
+
assert.equal(metadata.inputs.targetFramework, '.NETFramework,Version=v4.6.2');
|
|
73
|
+
assert.equal(metadata.inputs.platform, 'AnyCPU');
|
|
74
|
+
assert.deepEqual(Object.keys(metadata.inputs.sourceSha256).sort(), sources);
|
|
75
|
+
assert.deepEqual(Object.keys(metadata.inputs.referenceSha256).sort(),
|
|
76
|
+
['System.Core.dll', 'System.Management.dll', 'System.Web.Extensions.dll', 'System.dll', 'mscorlib.dll']);
|
|
77
|
+
assert.match(metadata.inputs.compilerSha256, /^[a-f0-9]{64}$/);
|
|
78
|
+
assert.deepEqual(metadata.inputs.compilerArguments, [
|
|
79
|
+
'/nologo', '/noconfig', '/nostdlib+', '/target:exe', '/platform:anycpu', '/langversion:7.3',
|
|
80
|
+
'/optimize+', '/debug-', '/deterministic+', '/checked-', '/unsafe-', '/warn:4', '/warnaserror+', '/utf8output',
|
|
81
|
+
]);
|
|
82
|
+
assert.equal(files.find((file) => file.path.endsWith('.exe')).sha256, metadata.binarySha256);
|
|
83
|
+
const inputs = sources.map((name) => [`bin/windows-legacy/src/${name}`, metadata.inputs.sourceSha256[name]]);
|
|
84
|
+
inputs.push(['tools/windows-legacy/build.ps1', metadata.inputs.buildScriptSha256]);
|
|
85
|
+
for (const [path, expected] of inputs) {
|
|
86
|
+
assert.equal(hash(readFileSync(join(root, path), 'utf8').replaceAll('\r\n', '\n')), expected,
|
|
87
|
+
`Changed legacy broker input: ${path}`);
|
|
88
|
+
}
|
|
89
|
+
return { status: 'source-and-binary-hashes-verified', files,
|
|
90
|
+
binarySha256: metadata.binarySha256, compilerSha256: metadata.inputs.compilerSha256,
|
|
91
|
+
reproducibilityBuild: 'requires-recorded-toolchain-verify' };
|
|
92
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Windows process lifetime helper
|
|
2
|
+
|
|
3
|
+
The bridge awaits this packaged, own-source .NET Framework 4.6.2 program before
|
|
4
|
+
reading config, listening, or spawning workloads. It has no npm dependencies,
|
|
5
|
+
runtime compilation, PowerShell invocation, downloads, elevation, or service setup.
|
|
6
|
+
|
|
7
|
+
Before **each owner and observer launch**, the Node module hashes the actual
|
|
8
|
+
executable bytes and compares them with the packaged metadata's recorded SHA256.
|
|
9
|
+
Missing files, invalid metadata and mismatched bytes fail before native spawn.
|
|
10
|
+
This is a packaged-identity guard, not protection against a same-user actor
|
|
11
|
+
replacing both the executable and metadata, or replacing the whole package.
|
|
12
|
+
|
|
13
|
+
## Ownership
|
|
14
|
+
|
|
15
|
+
The helper opens its **actual parent** from native process information and retains
|
|
16
|
+
that process handle. It requires `watch-parent <caller PID>` and checks that PID
|
|
17
|
+
against its actual parent; it cannot select an unrelated target. Bare invocation
|
|
18
|
+
fails before assigning any process. This is an internal bridge launch contract,
|
|
19
|
+
not a standalone command to run in a shell.
|
|
20
|
+
|
|
21
|
+
Creation-time ordering rejects a reused parent PID, and an already exited parent
|
|
22
|
+
is refused. It creates an unnamed, non-inheritable Job Object
|
|
23
|
+
with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, without either breakaway flag, assigns
|
|
24
|
+
the bridge, verifies membership, and only then writes `MCP_JOB_READY`.
|
|
25
|
+
|
|
26
|
+
The bridge is not suspended: it is awaiting that acknowledgement and cannot start
|
|
27
|
+
any work yet. Every later Windows child inherits the job **during process
|
|
28
|
+
creation**, before its first instruction. This covers isolated, pool, shared,
|
|
29
|
+
auth-command, and config-write helper spawns, including cmd/npx wrappers. There is
|
|
30
|
+
no start-then-assign race, post-crash PID sweep, process enumeration in production,
|
|
31
|
+
or fallback to uncontained spawning.
|
|
32
|
+
|
|
33
|
+
The owner was started before the bridge joined this job and remains outside it.
|
|
34
|
+
It alone holds the non-inheritable job handle. On bridge exit, the held process
|
|
35
|
+
handle signals and the owner closes the job, terminating surviving descendants.
|
|
36
|
+
Both native roles use detached Windows launches, so Node's automatic direct-child
|
|
37
|
+
termination does not kill the owner before it can drain. These are not unowned
|
|
38
|
+
daemons: each role watches its actual parent's held process handle and exits when
|
|
39
|
+
that parent is lost. Owner stdio/process references do not keep an otherwise
|
|
40
|
+
finished bridge alive.
|
|
41
|
+
Killing the owner also closes the job and terminates the bridge and descendants.
|
|
42
|
+
Normal DELETE/recycle still uses the existing per-session tree cleanup; shared
|
|
43
|
+
detach, counters and request replay rules are unchanged. POSIX does not launch
|
|
44
|
+
this helper or change its process behavior.
|
|
45
|
+
|
|
46
|
+
A bridge exit notification can precede final job teardown. This mechanism does
|
|
47
|
+
not adopt or clean processes abandoned by an earlier runtime.
|
|
48
|
+
|
|
49
|
+
## Verified managed stop
|
|
50
|
+
|
|
51
|
+
`ensureWindowsProcessLifetime()` returns
|
|
52
|
+
`{protocol:1, ownerPid, ownerCreationTime}` on Windows (`null` on POSIX).
|
|
53
|
+
The creation identity is a decimal **Windows FILETIME string**, not a rounded
|
|
54
|
+
JavaScript timestamp or .NET DateTime tick count. The identity comes from the
|
|
55
|
+
native owner after assignment and its PID must match the spawned child.
|
|
56
|
+
|
|
57
|
+
The supervisor calls `await observeWindowsProcessLifetime(identity)` before
|
|
58
|
+
declaring the bridge ready or permitting stop. It returns `{done, pid}` after a
|
|
59
|
+
separate native observer has opened the owner, checked its exact creation time
|
|
60
|
+
and same helper image, and retained that process handle. It also opens a
|
|
61
|
+
read-only synchronization handle to the owner's initially-unsignaled drain
|
|
62
|
+
event. The owner creates that event before readiness, names it from its PID and
|
|
63
|
+
creation time, and refuses a pre-existing event. Neither handle is inherited.
|
|
64
|
+
The observer is outside
|
|
65
|
+
the bridge job, read-only, and also watches its own parent: losing the supervisor
|
|
66
|
+
does not orphan the observer. Unavailable/reused identities fail closed.
|
|
67
|
+
|
|
68
|
+
After root exit the owner calls `TerminateJobObject`, then queries actual job
|
|
69
|
+
accounting until `ActiveProcesses == 0`, bounded to five seconds. Only successful
|
|
70
|
+
drain signals that held event and returns exit code zero. Kill-on-close remains
|
|
71
|
+
the failure backstop. The observer waits on the held owner handle, checks both
|
|
72
|
+
successful exit and the signaled event, emits the
|
|
73
|
+
drain receipt, then exits itself. Only after observer exit and complete output
|
|
74
|
+
does `done` resolve to `{verified:true, ownerExited:true, activeProcesses:0}`.
|
|
75
|
+
Forced owner death (including a forced exit code zero) or a failed drain rejects
|
|
76
|
+
`done`; PID disappearance or an exit code alone is never accepted as proof.
|
|
77
|
+
These session-local kernel events leave no files or persistent settings.
|
|
78
|
+
|
|
79
|
+
Managed startup sends the owner identity early and waits for the supervisor's
|
|
80
|
+
observer acknowledgement before config initialization. This avoids losing the
|
|
81
|
+
owner before failed-start cleanup can arm observation. Budget root exit and
|
|
82
|
+
`observer.done` inside one existing stop deadline, not two sequential deadlines.
|
|
83
|
+
|
|
84
|
+
Windows 10/11 and Server 2016+ with .NET Framework 4.6.2+ are required. An outer job
|
|
85
|
+
whose restrictions prevent nested assignment causes explicit startup failure,
|
|
86
|
+
not an uncontained bridge. ARM64 execution remains an outstanding platform gate.
|
|
87
|
+
The historical configuration security helper is separate and unchanged.
|
|
88
|
+
|
|
89
|
+
## Build and verify
|
|
90
|
+
|
|
91
|
+
Use the already installed Visual Studio Roslyn compiler and 4.6.2 targeting pack:
|
|
92
|
+
|
|
93
|
+
```powershell
|
|
94
|
+
.\tools\windows-process-lifetime\build.ps1
|
|
95
|
+
.\tools\windows-process-lifetime\build.ps1 -Verify
|
|
96
|
+
# Optional: select the exact recorded, installed compiler/reference directory.
|
|
97
|
+
.\tools\windows-process-lifetime\build.ps1 -Verify -CompilerPath C:\path\to\Roslyn\csc.exe
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Both modes compile twice in different owned temporary directories and require
|
|
101
|
+
identical bytes. Verification also requires the recorded compiler, references,
|
|
102
|
+
normalized source/build-script hashes and packaged binary hash; it updates
|
|
103
|
+
nothing. Builds install no tools and clean their explicitly recorded scratch
|
|
104
|
+
files. Only the entry point and P/Invoke declarations are static.
|
|
105
|
+
|
|
106
|
+
`inventory.mjs` checks the exact separate asset layout and normalized build inputs
|
|
107
|
+
on every platform. It does not substitute for the byte-for-byte Windows rebuild.
|
|
108
|
+
Package/native gates require these assets for current versions starting at 2.0.2;
|
|
109
|
+
the immutable 1.3.1 security-helper baseline is not relabeled or modified.
|
|
110
|
+
|
|
111
|
+
## Regression
|
|
112
|
+
|
|
113
|
+
`test/windows-process-lifetime.test.mjs` uses only owned synthetic nested
|
|
114
|
+
`cmd -> Node stand-in -> cmd -> Node worker` processes, dynamic loopback TCP
|
|
115
|
+
heartbeats, MCP readiness, 45-second expiry backstops and captured
|
|
116
|
+
PID/creation-time/held-handle cleanup. DELETE/recycle are controls; abrupt bridge
|
|
117
|
+
loss must stop both heartbeats and terminate every captured descendant. Evidence
|
|
118
|
+
is retained outside Git; set `MCP_LIFETIME_TEST_EVIDENCE` to an existing private
|
|
119
|
+
evidence parent. The known historical live bridge exit trigger remains unknown.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Builds or verifies the separate Windows process lifetime helper.
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Uses installed Visual Studio Roslyn and .NET Framework 4.6.2 reference assemblies.
|
|
6
|
+
Compiles twice under different paths, records every input, and downloads nothing.
|
|
7
|
+
#>
|
|
8
|
+
[CmdletBinding()]
|
|
9
|
+
param(
|
|
10
|
+
[switch]$Verify,
|
|
11
|
+
[string]$CompilerPath,
|
|
12
|
+
[string]$ReferenceAssemblyPath
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Set-StrictMode -Version Latest
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
|
|
18
|
+
function Get-Digest([string]$Path, [switch]$Text) {
|
|
19
|
+
$bytes = [IO.File]::ReadAllBytes($Path)
|
|
20
|
+
if ($Text) {
|
|
21
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes([IO.File]::ReadAllText($Path).Replace("`r`n", "`n"))
|
|
22
|
+
}
|
|
23
|
+
$hash = [Security.Cryptography.SHA256]::Create()
|
|
24
|
+
try { return [BitConverter]::ToString($hash.ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() }
|
|
25
|
+
finally { $hash.Dispose() }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if ($env:OS -ne 'Windows_NT') { throw 'Build on Windows with installed Roslyn and .NET Framework 4.6.2 references.' }
|
|
29
|
+
$root = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
|
30
|
+
$directory = Join-Path $root 'bin\windows-lifetime'
|
|
31
|
+
$binary = 'ProcessLifetimeHelper.exe'
|
|
32
|
+
$metadataPath = Join-Path $directory 'ProcessLifetimeHelper.build.json'
|
|
33
|
+
$sources = @('AssemblyInfo.cs', 'ProcessLifetimeHelper.cs')
|
|
34
|
+
$references = @('mscorlib.dll', 'System.dll')
|
|
35
|
+
if (-not $CompilerPath) {
|
|
36
|
+
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
|
37
|
+
if (-not (Test-Path -LiteralPath $vswhere)) { throw 'Specify an installed Roslyn -CompilerPath; no tools are installed.' }
|
|
38
|
+
$installation = & $vswhere -latest -products '*' -requires Microsoft.Component.MSBuild -property installationPath
|
|
39
|
+
if ($LASTEXITCODE -ne 0 -or
|
|
40
|
+
-not $installation) { throw 'No installed Visual Studio MSBuild compiler found.' }
|
|
41
|
+
$CompilerPath = Join-Path $installation 'MSBuild\Current\Bin\Roslyn\csc.exe'
|
|
42
|
+
}
|
|
43
|
+
if (-not $ReferenceAssemblyPath) {
|
|
44
|
+
$ReferenceAssemblyPath = Join-Path ${env:ProgramFiles(x86)} 'Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.2'
|
|
45
|
+
}
|
|
46
|
+
$CompilerPath = (Resolve-Path -LiteralPath $CompilerPath).Path
|
|
47
|
+
$ReferenceAssemblyPath = (Resolve-Path -LiteralPath $ReferenceAssemblyPath).Path
|
|
48
|
+
$framework = [xml][IO.File]::ReadAllText((Join-Path $ReferenceAssemblyPath 'RedistList\FrameworkList.xml'))
|
|
49
|
+
if ($framework.FileList.Name -cne '.NET Framework 4.6.2') { throw 'Wrong reference assembly targeting pack.' }
|
|
50
|
+
$version = (& $CompilerPath /version | Out-String).Trim()
|
|
51
|
+
if ($LASTEXITCODE -ne 0) { throw 'Roslyn version probe failed.' }
|
|
52
|
+
$options = @('/nologo', '/noconfig', '/nostdlib+', '/target:exe', '/platform:anycpu',
|
|
53
|
+
'/langversion:7.3', '/optimize+', '/debug-', '/deterministic+', '/checked-',
|
|
54
|
+
'/unsafe-', '/warn:4', '/warnaserror+', '/utf8output')
|
|
55
|
+
$sourceHashes = [ordered]@{}
|
|
56
|
+
foreach ($name in $sources) { $sourceHashes[$name] = Get-Digest (Join-Path $directory "src\$name") -Text }
|
|
57
|
+
$referenceHashes = [ordered]@{}
|
|
58
|
+
foreach ($name in $references) { $referenceHashes[$name] = Get-Digest (Join-Path $ReferenceAssemblyPath $name) }
|
|
59
|
+
$inputs = [ordered]@{
|
|
60
|
+
targetFramework = '.NETFramework,Version=v4.6.2'
|
|
61
|
+
platform = 'AnyCPU'
|
|
62
|
+
compilerVersion = $version
|
|
63
|
+
compilerSha256 = Get-Digest $CompilerPath
|
|
64
|
+
compilerArguments = $options
|
|
65
|
+
pathMap = '/_/mcp-pacemaker/windows-process-lifetime'
|
|
66
|
+
sourceEncoding = 'UTF-8 without BOM, LF line endings'
|
|
67
|
+
sourceSha256 = $sourceHashes
|
|
68
|
+
buildScriptSha256 = Get-Digest $PSCommandPath -Text
|
|
69
|
+
referenceSha256 = $referenceHashes
|
|
70
|
+
}
|
|
71
|
+
$expected = $null
|
|
72
|
+
if ($Verify) {
|
|
73
|
+
$expected = [IO.File]::ReadAllText($metadataPath) | ConvertFrom-Json
|
|
74
|
+
if ($expected.schemaVersion -ne 1 -or
|
|
75
|
+
$expected.binary -cne $binary -or
|
|
76
|
+
(ConvertTo-Json $inputs -Depth 6 -Compress) -cne (ConvertTo-Json $expected.inputs -Depth 6 -Compress) -or
|
|
77
|
+
(Get-Digest (Join-Path $directory $binary)) -cne $expected.binarySha256) {
|
|
78
|
+
throw 'Packaged lifetime helper inputs/toolchain/bytes do not match metadata. Verification changes nothing.'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
$temporary = Join-Path ([IO.Path]::GetTempPath()) ('mcp-lifetime-build-' + [Guid]::NewGuid().ToString('N'))
|
|
82
|
+
$files = [Collections.Generic.List[string]]::new()
|
|
83
|
+
$directories = [Collections.Generic.List[string]]::new()
|
|
84
|
+
$outputs = [Collections.Generic.List[string]]::new()
|
|
85
|
+
try {
|
|
86
|
+
$null = [IO.Directory]::CreateDirectory($temporary)
|
|
87
|
+
$directories.Add($temporary)
|
|
88
|
+
foreach ($pass in @('first', 'second')) {
|
|
89
|
+
$scratch = Join-Path $temporary $pass
|
|
90
|
+
$null = [IO.Directory]::CreateDirectory($scratch)
|
|
91
|
+
$directories.Add($scratch)
|
|
92
|
+
$paths = foreach ($name in $sources) {
|
|
93
|
+
$path = Join-Path $scratch $name
|
|
94
|
+
$files.Add($path)
|
|
95
|
+
$text = [IO.File]::ReadAllText((Join-Path $directory "src\$name")).Replace("`r`n", "`n")
|
|
96
|
+
[IO.File]::WriteAllText($path, $text, [Text.UTF8Encoding]::new($false))
|
|
97
|
+
$path
|
|
98
|
+
}
|
|
99
|
+
$output = Join-Path $scratch $binary
|
|
100
|
+
$files.Add($output)
|
|
101
|
+
$arguments = $options + @("/out:$output", "/pathmap:$scratch=$($inputs.pathMap)")
|
|
102
|
+
foreach ($name in $references) { $arguments += '/reference:' + (Join-Path $ReferenceAssemblyPath $name) }
|
|
103
|
+
& $CompilerPath @arguments @paths
|
|
104
|
+
if ($LASTEXITCODE -ne 0) { throw "Lifetime helper compile failed: $pass" }
|
|
105
|
+
$outputs.Add($output)
|
|
106
|
+
}
|
|
107
|
+
$digest = Get-Digest $outputs[0]
|
|
108
|
+
if ($digest -cne (Get-Digest $outputs[1])) { throw 'Two builds differ; no package assets changed.' }
|
|
109
|
+
if ($Verify) {
|
|
110
|
+
if ($digest -cne $expected.binarySha256) { throw 'Rebuilt bytes differ; no package assets changed.' }
|
|
111
|
+
Write-Output "Verified lifetime helper source, toolchain and byte-for-byte rebuild: SHA256 $digest"
|
|
112
|
+
} else {
|
|
113
|
+
$metadata = [ordered]@{ schemaVersion = 1; binary = $binary; binarySha256 = $digest; inputs = $inputs }
|
|
114
|
+
[IO.File]::Copy($outputs[0], (Join-Path $directory $binary), $true)
|
|
115
|
+
[IO.File]::WriteAllText($metadataPath,
|
|
116
|
+
(ConvertTo-Json $metadata -Depth 8).Replace("`r`n", "`n") + "`n", [Text.UTF8Encoding]::new($false))
|
|
117
|
+
Write-Output "Built two matching lifetime helpers with Roslyn ${version}: SHA256 $digest"
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
foreach ($path in $files) { Remove-Item -LiteralPath $path -ErrorAction SilentlyContinue }
|
|
121
|
+
for ($index = $directories.Count - 1; $index -ge 0; $index--) { [IO.Directory]::Delete($directories[$index]) }
|
|
122
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const LIFETIME_FILES = Object.freeze([
|
|
7
|
+
'bin/windows-process-lifetime.mjs',
|
|
8
|
+
'bin/windows-lifetime/ProcessLifetimeHelper.exe',
|
|
9
|
+
'bin/windows-lifetime/ProcessLifetimeHelper.build.json',
|
|
10
|
+
'bin/windows-lifetime/src/AssemblyInfo.cs',
|
|
11
|
+
'bin/windows-lifetime/src/ProcessLifetimeHelper.cs',
|
|
12
|
+
'tools/windows-process-lifetime/build.ps1',
|
|
13
|
+
'tools/windows-process-lifetime/inventory.mjs',
|
|
14
|
+
'tools/windows-process-lifetime/README.md',
|
|
15
|
+
]);
|
|
16
|
+
export const LIFETIME_TEST = 'test/windows-process-lifetime.test.mjs';
|
|
17
|
+
export const LIFETIME_STARTUP_TEST = 'test/windows-lifetime-startup.test.mjs';
|
|
18
|
+
export const LIFETIME_IDENTITY_TEST = 'test/windows-lifetime-identity.test.mjs';
|
|
19
|
+
export const LIFETIME_REQUIRED_TESTS = Object.freeze([
|
|
20
|
+
...['normal-delete', 'recycle', 'abrupt-root-loss', 'shared-abrupt', 'concurrent-abrupt',
|
|
21
|
+
'pool-abrupt', 'auth-abrupt', 'owner-loss', 'supervisor-restart']
|
|
22
|
+
.map((mode) => `Windows lifetime: ${mode} leaves no nested descendants`),
|
|
23
|
+
...['gated-success', 'missing', 'invalid', 'assignment-failure', 'observer-owner-loss',
|
|
24
|
+
'observer-stale', 'observer-wrong-image', 'observer-parent-loss', 'observer-zero-exit']
|
|
25
|
+
.map((mode) => `Windows lifetime startup: ${mode}`),
|
|
26
|
+
'lifetime helper bare or unrelated-parent invocation cannot assign the caller',
|
|
27
|
+
...['missing', 'altered', 'stale', 'protocol'].flatMap((kind) =>
|
|
28
|
+
['owner', 'observer'].map((action) => `packaged helper identity rejects ${kind} before ${action} spawn`)),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function requiresProcessLifetime(version) {
|
|
32
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
33
|
+
assert.ok(match, 'Lifetime gate requires a canonical stable version');
|
|
34
|
+
return Number(match[1]) > 2 ||
|
|
35
|
+
Number(match[1]) === 2 &&
|
|
36
|
+
(Number(match[2]) > 0 || Number(match[3]) >= 2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function verifyProcessLifetimeAssets(root, version) {
|
|
40
|
+
if (!requiresProcessLifetime(version)) return { status: 'not-required-for-historical-version' };
|
|
41
|
+
for (const path of ['bin', 'bin/windows-lifetime', 'bin/windows-lifetime/src',
|
|
42
|
+
'tools', 'tools/windows-process-lifetime']) {
|
|
43
|
+
const stat = lstatSync(join(root, path));
|
|
44
|
+
assert.ok(stat.isDirectory() &&
|
|
45
|
+
!stat.isSymbolicLink(), `Linked or invalid lifetime asset directory: ${path}`);
|
|
46
|
+
}
|
|
47
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
48
|
+
const files = LIFETIME_FILES.map((path) => {
|
|
49
|
+
const full = join(root, path);
|
|
50
|
+
const stat = lstatSync(full);
|
|
51
|
+
assert.ok(stat.isFile() &&
|
|
52
|
+
!stat.isSymbolicLink() &&
|
|
53
|
+
stat.size > 0, `Invalid lifetime package asset: ${path}`);
|
|
54
|
+
return { path, sha256: hash(readFileSync(full)) };
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(readdirSync(join(root, 'bin/windows-lifetime')).sort(), [
|
|
57
|
+
'ProcessLifetimeHelper.build.json', 'ProcessLifetimeHelper.exe', 'src',
|
|
58
|
+
]);
|
|
59
|
+
assert.deepEqual(readdirSync(join(root, 'bin/windows-lifetime/src')).sort(), [
|
|
60
|
+
'AssemblyInfo.cs', 'ProcessLifetimeHelper.cs',
|
|
61
|
+
]);
|
|
62
|
+
const metadata = JSON.parse(readFileSync(join(root, 'bin/windows-lifetime/ProcessLifetimeHelper.build.json')));
|
|
63
|
+
assert.equal(metadata.schemaVersion, 1);
|
|
64
|
+
assert.equal(metadata.binary, 'ProcessLifetimeHelper.exe');
|
|
65
|
+
assert.equal(metadata.inputs.targetFramework, '.NETFramework,Version=v4.6.2');
|
|
66
|
+
assert.equal(metadata.inputs.platform, 'AnyCPU');
|
|
67
|
+
assert.deepEqual(Object.keys(metadata.inputs.sourceSha256).sort(), ['AssemblyInfo.cs', 'ProcessLifetimeHelper.cs']);
|
|
68
|
+
assert.deepEqual(Object.keys(metadata.inputs.referenceSha256).sort(), ['System.dll', 'mscorlib.dll']);
|
|
69
|
+
assert.match(metadata.inputs.compilerSha256, /^[a-f0-9]{64}$/);
|
|
70
|
+
assert.deepEqual(metadata.inputs.compilerArguments, [
|
|
71
|
+
'/nologo', '/noconfig', '/nostdlib+', '/target:exe', '/platform:anycpu',
|
|
72
|
+
'/langversion:7.3', '/optimize+', '/debug-', '/deterministic+', '/checked-',
|
|
73
|
+
'/unsafe-', '/warn:4', '/warnaserror+', '/utf8output',
|
|
74
|
+
]);
|
|
75
|
+
assert.equal(files.find((file) => file.path.endsWith('.exe')).sha256, metadata.binarySha256);
|
|
76
|
+
const sources = Object.entries(metadata.inputs.sourceSha256)
|
|
77
|
+
.map(([name, expected]) => [`bin/windows-lifetime/src/${name}`, expected]);
|
|
78
|
+
sources.push(['tools/windows-process-lifetime/build.ps1', metadata.inputs.buildScriptSha256]);
|
|
79
|
+
for (const [path, expected] of sources) {
|
|
80
|
+
assert.equal(hash(readFileSync(join(root, path), 'utf8').replaceAll('\r\n', '\n')), expected,
|
|
81
|
+
`Lifetime helper build input changed: ${path}`);
|
|
82
|
+
}
|
|
83
|
+
return { status: 'source-and-binary-hashes-verified', files,
|
|
84
|
+
compilerSha256: metadata.inputs.compilerSha256, binarySha256: metadata.binarySha256,
|
|
85
|
+
reproducibilityBuild: 'requires-recorded-toolchain-verify' };
|
|
86
|
+
}
|