@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba7d36726eda

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +5 -1
  3. package/dist/bridge/bridge.service.d.ts +52 -3
  4. package/dist/bridge/bridge.service.js +776 -19
  5. package/dist/bridge/bridge.types.d.ts +131 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.js +46 -0
  14. package/dist/cli/cli.service.d.ts +42 -2
  15. package/dist/cli/cli.service.js +350 -23
  16. package/dist/cli/local-config.d.ts +4 -0
  17. package/dist/cli/local-config.js +39 -0
  18. package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
  19. package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
  20. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  22. package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
  23. package/dist/runtime/attempt-process-supervisor.js +290 -0
  24. package/dist/runtime/host-listener-inspector.d.ts +28 -0
  25. package/dist/runtime/host-listener-inspector.js +185 -0
  26. package/dist/runtime/host-port-broker.d.ts +76 -0
  27. package/dist/runtime/host-port-broker.js +333 -0
  28. package/dist/runtime/host-runtime-contract.d.ts +39 -0
  29. package/dist/runtime/host-runtime-contract.js +295 -0
  30. package/dist/runtime/host-runtime-lease.d.ts +122 -0
  31. package/dist/runtime/host-runtime-lease.js +1241 -0
  32. package/dist/runtime/host-runtime-promotion.d.ts +21 -0
  33. package/dist/runtime/host-runtime-promotion.js +128 -0
  34. package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
  35. package/dist/runtime/node-host-runtime-adapter.js +205 -0
  36. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  37. package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
  38. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  39. package/dist/runtime/runtime-isolation-lease.js +158 -0
  40. package/dist/sandbox/command-runner.d.ts +4 -0
  41. package/dist/sandbox/command-runner.js +47 -0
  42. package/dist/sandbox/contract.d.ts +5 -0
  43. package/dist/sandbox/contract.js +276 -0
  44. package/dist/sandbox/event-budget.d.ts +11 -0
  45. package/dist/sandbox/event-budget.js +27 -0
  46. package/dist/sandbox/image-policy.d.ts +35 -0
  47. package/dist/sandbox/image-policy.js +247 -0
  48. package/dist/sandbox/index.d.ts +5 -0
  49. package/dist/sandbox/index.js +5 -0
  50. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  51. package/dist/sandbox/lease-watchdog.js +67 -0
  52. package/dist/sandbox/preflight.d.ts +45 -0
  53. package/dist/sandbox/preflight.js +300 -0
  54. package/dist/sandbox/process.d.ts +20 -0
  55. package/dist/sandbox/process.js +209 -0
  56. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  57. package/dist/sandbox/provider-auth/codex.js +212 -0
  58. package/dist/sandbox/repository-lock.d.ts +5 -0
  59. package/dist/sandbox/repository-lock.js +119 -0
  60. package/dist/sandbox/runtime.d.ts +297 -0
  61. package/dist/sandbox/runtime.js +2338 -0
  62. package/dist/sandbox/sandbox.types.d.ts +86 -0
  63. package/dist/sandbox/sandbox.types.js +1 -0
  64. package/dist/sandbox/smoke.d.ts +76 -0
  65. package/dist/sandbox/smoke.js +380 -0
  66. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  67. package/dist/sandbox/worker-supervisor.js +36 -0
  68. package/dist/sandbox/worker.d.ts +1 -0
  69. package/dist/sandbox/worker.js +225 -0
  70. package/dist/sandbox/workspace.service.d.ts +13 -0
  71. package/dist/sandbox/workspace.service.js +67 -0
  72. package/dist/session/phase-completion.artifacts.js +23 -2
  73. package/dist/session/phase-completion.helpers.js +6 -1
  74. package/dist/session/phase-completion.runtime.js +3 -1
  75. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
  76. package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
  77. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  78. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
  79. package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
  80. package/dist/session/provider-cli/provider-cli.process.d.ts +3 -0
  81. package/dist/session/provider-cli/provider-cli.process.js +65 -14
  82. package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
  83. package/dist/session/provider-cli/provider-cli.prompts.js +99 -15
  84. package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
  85. package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
  86. package/dist/session/provider-cli/provider-cli.service.d.ts +4 -1
  87. package/dist/session/provider-cli/provider-cli.service.js +167 -28
  88. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  89. package/dist/session/session-redaction.d.ts +3 -0
  90. package/dist/session/session-redaction.js +23 -0
  91. package/dist/session/session.base.d.ts +4 -0
  92. package/dist/session/session.service.d.ts +6 -1
  93. package/dist/session/session.service.js +184 -38
  94. package/dist/session/session.types.d.ts +58 -1
  95. package/dist/session/session.types.js +172 -2
  96. package/dist/shared/path/path.js +3 -1
  97. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  98. package/dist/shared/process/host-process-recovery.js +78 -0
  99. package/dist/shared/process/host-process-supervisor.d.ts +40 -0
  100. package/dist/shared/process/host-process-supervisor.js +195 -0
  101. package/dist/shared/provider-capabilities.d.ts +1 -0
  102. package/dist/shared/provider-capabilities.js +3 -0
  103. package/dist/shared/redaction/redaction.js +6 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/dist/worktree/index.d.ts +1 -0
  107. package/dist/worktree/index.js +1 -0
  108. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  109. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  110. package/dist/worktree/worktree-paths.d.ts +4 -0
  111. package/dist/worktree/worktree-paths.js +30 -0
  112. package/dist/worktree/worktree.base.d.ts +3 -0
  113. package/dist/worktree/worktree.service.d.ts +6 -2
  114. package/dist/worktree/worktree.service.js +60 -43
  115. package/dist/worktree/worktree.types.d.ts +3 -1
  116. package/package.json +6 -3
  117. package/runtime/windows-job-supervisor.ps1 +287 -0
  118. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -1,53 +1,61 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { RepositoryCoordinatorLock } from "../sandbox/repository-lock.js";
3
4
  import { GitClient } from "../shared/git/git.js";
4
5
  import { ensureDirectory, isSubPath, resolveExistingDirectoryPath, safeSegment } from "../shared/path/path.js";
5
6
  import { filterArtifactsForLane, injectWorktreeArtifacts } from "./artifact-injector.js";
7
+ import { hydratePriorPhaseArtifacts } from "./prior-phase-artifact-hydrator.js";
6
8
  import { warmWorktreeDependencies } from "./dependency-warmer.js";
9
+ import { defaultWorktreeCoordinatorStateRoot, managedRepoRoot, resolveManagedWorktreeRoot, } from "./worktree-paths.js";
7
10
  export class GitWorktreeService {
8
11
  git;
9
- constructor(git = new GitClient()) {
12
+ coordinator;
13
+ constructor(git = new GitClient(), coordinator = new RepositoryCoordinatorLock(defaultWorktreeCoordinatorStateRoot())) {
10
14
  this.git = git;
15
+ this.coordinator = coordinator;
11
16
  }
12
17
  async prepare(request) {
13
18
  const allowedRoot = await ensureDirectory(request.allowedRepoRoot);
14
19
  const baseBranch = normalizeBranchName(request.baseBranch, "develop");
15
- const repoPath = await this.resolveRepoPath(request.repo, allowedRoot, baseBranch);
16
- await this.ensureGitIdentity(repoPath);
17
- const baseStartPoint = await this.resolveBaseStartPoint(repoPath, baseBranch);
18
20
  const shortId = request.workItem.id.replace(/-/g, "").slice(0, 12);
19
21
  const kind = safeSegment(request.workItem.kind, "work-item");
20
22
  const fallbackWorktreeKey = `${kind}/${shortId}`;
21
23
  const worktreeKey = request.worktreeKey?.trim() || fallbackWorktreeKey;
22
24
  const worktreeSegments = worktreeKeySegments(worktreeKey, kind, shortId);
23
25
  const branchName = normalizeBranchName(worktreeSegments.join("/"), "work-item/unknown");
24
- const worktreeRoot = resolveWorktreeRoot(allowedRoot, repoPath);
25
- const worktreePath = path.join(worktreeRoot, ...worktreeSegments);
26
- const resolvedWorktreeParent = await ensureDirectory(path.dirname(worktreePath));
27
- if (!isSubPath(allowedRoot, resolvedWorktreeParent)) {
28
- throw new Error(`Worktree path must stay under ${allowedRoot}.`);
29
- }
30
- const exists = await directoryExists(worktreePath);
31
- let resolvedWorktreePath;
32
- if (!exists) {
33
- await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseStartPoint]);
34
- resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
35
- }
36
- else {
37
- resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
38
- if (!isSubPath(allowedRoot, resolvedWorktreePath)) {
26
+ const creationCoordinationPath = this.repoCreationCoordinationPath(request.repo, allowedRoot);
27
+ const repoPath = await this.coordinator.withLock(creationCoordinationPath, async () => {
28
+ return await this.resolveRepoPath(request.repo, allowedRoot, baseBranch);
29
+ });
30
+ const resolvedWorktreePath = await this.coordinator.withLock(repoPath, async () => {
31
+ await this.ensureGitIdentity(repoPath);
32
+ const baseStartPoint = await this.resolveBaseStartPoint(repoPath, baseBranch);
33
+ const worktreeRoot = resolveManagedWorktreeRoot(allowedRoot, repoPath);
34
+ const worktreePath = path.join(worktreeRoot, ...worktreeSegments);
35
+ const resolvedWorktreeParent = await ensureDirectory(path.dirname(worktreePath));
36
+ if (!isSubPath(allowedRoot, resolvedWorktreeParent)) {
37
+ throw new Error(`Worktree path must stay under ${allowedRoot}.`);
38
+ }
39
+ if (!(await directoryExists(worktreePath))) {
40
+ await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseStartPoint]);
41
+ return await resolveExistingDirectoryPath(worktreePath);
42
+ }
43
+ const existingWorktreePath = await resolveExistingDirectoryPath(worktreePath);
44
+ if (!isSubPath(allowedRoot, existingWorktreePath)) {
39
45
  throw new Error(`Worktree path must stay under ${allowedRoot}.`);
40
46
  }
41
- const currentBranch = await this.currentBranch(resolvedWorktreePath);
47
+ const currentBranch = await this.currentBranch(existingWorktreePath);
42
48
  if (currentBranch !== branchName) {
43
- throw new Error(`Existing worktree ${resolvedWorktreePath} is on branch '${currentBranch}', expected '${branchName}'.`);
49
+ throw new Error(`Existing worktree ${existingWorktreePath} is on branch '${currentBranch}', expected '${branchName}'.`);
44
50
  }
45
51
  if (shouldResetExistingWorktree(request)) {
46
- await this.resetWorktreeToBase(resolvedWorktreePath, baseStartPoint);
52
+ await this.resetWorktreeToBase(existingWorktreePath, baseStartPoint);
47
53
  }
48
- }
54
+ return existingWorktreePath;
55
+ });
49
56
  await this.ensureGitIdentity(resolvedWorktreePath);
50
57
  await clearRunArtifactRoots(resolvedWorktreePath, request);
58
+ const hydratedPriorPhaseArtifactCount = await hydratePriorPhaseArtifacts(resolvedWorktreePath, request.priorPhaseArtifacts);
51
59
  const qaReworkPacketPath = await materializeQaReworkPacket(resolvedWorktreePath, request);
52
60
  const artifacts = filterArtifactsForLane(request.artifacts, request.lane, request.phase);
53
61
  if (artifacts.length > 0) {
@@ -66,20 +74,32 @@ export class GitWorktreeService {
66
74
  baseBranch,
67
75
  cleanupWorktrees: request.cleanupWorktrees,
68
76
  dependencyWarm,
69
- qaReworkPacketPath
77
+ qaReworkPacketPath,
78
+ hydratedPriorPhaseArtifactCount
70
79
  };
71
80
  }
81
+ repoCreationCoordinationPath(repo, allowedRoot) {
82
+ if (repo.serverLocalPath && repo.serverLocalPath.trim().length > 0) {
83
+ return path.resolve(repo.serverLocalPath);
84
+ }
85
+ return this.managedRepoPath(repo, allowedRoot);
86
+ }
72
87
  async release(handle) {
73
- if (!handle.cleanupWorktrees || !(await directoryExists(handle.worktreePath))) {
88
+ if (!handle.cleanupWorktrees) {
74
89
  return;
75
90
  }
76
- try {
77
- await this.git.run(handle.repoPath, ["worktree", "remove", "--force", handle.worktreePath]);
78
- await this.git.run(handle.repoPath, ["worktree", "prune"]);
79
- }
80
- catch {
81
- await fs.rm(handle.worktreePath, { recursive: true, force: true });
82
- }
91
+ await this.coordinator.withLock(handle.repoPath, async () => {
92
+ if (!(await directoryExists(handle.worktreePath))) {
93
+ return;
94
+ }
95
+ try {
96
+ await this.git.run(handle.repoPath, ["worktree", "remove", "--force", handle.worktreePath]);
97
+ await this.git.run(handle.repoPath, ["worktree", "prune"]);
98
+ }
99
+ catch {
100
+ await fs.rm(handle.worktreePath, { recursive: true, force: true });
101
+ }
102
+ });
83
103
  }
84
104
  async resolveRepoPath(repo, allowedRoot, baseBranch) {
85
105
  if (repo.serverLocalPath && repo.serverLocalPath.trim().length > 0) {
@@ -96,7 +116,7 @@ export class GitWorktreeService {
96
116
  if (discovered) {
97
117
  return discovered;
98
118
  }
99
- const repoPath = path.join(managedRepoRoot(allowedRoot), `${safeSegment(repo.name, "repo")}-${repo.id.replace(/-/g, "")}`);
119
+ const repoPath = this.managedRepoPath(repo, allowedRoot);
100
120
  if (!(await gitMetadataExists(repoPath, allowedRoot))) {
101
121
  const repoPathExists = await pathExists(repoPath);
102
122
  if (repoPathExists) {
@@ -127,6 +147,9 @@ export class GitWorktreeService {
127
147
  }
128
148
  return resolved;
129
149
  }
150
+ managedRepoPath(repo, allowedRoot) {
151
+ return path.join(managedRepoRoot(allowedRoot), `${safeSegment(repo.name, "repo")}-${repo.id.replace(/-/g, "")}`);
152
+ }
130
153
  async findExistingRepoPath(repo, allowedRoot) {
131
154
  const expectedRemote = normalizeRemote(repo.remoteUrl);
132
155
  const rawName = repo.name.trim();
@@ -199,15 +222,6 @@ export class GitWorktreeService {
199
222
  await this.git.run(repoPath, ["config", "user.name", "Forge Bridge"]);
200
223
  }
201
224
  }
202
- function managedRepoRoot(allowedRoot) {
203
- return path.join(allowedRoot, ".forge-worktrees", "_repos");
204
- }
205
- function resolveWorktreeRoot(allowedRoot, repoPath) {
206
- const baseName = path.basename(repoPath);
207
- return isSubPath(managedRepoRoot(allowedRoot), repoPath)
208
- ? path.join(allowedRoot, ".forge-worktrees", baseName)
209
- : path.join(path.dirname(repoPath), ".forge-worktrees", baseName);
210
- }
211
225
  function isCloneableRemote(remoteUrl) {
212
226
  const trimmed = remoteUrl?.trim() ?? "";
213
227
  if (trimmed.length === 0 || trimmed.startsWith("local://")) {
@@ -215,9 +229,12 @@ function isCloneableRemote(remoteUrl) {
215
229
  }
216
230
  return trimmed.startsWith("git@") || trimmed.includes("://") || path.isAbsolute(trimmed);
217
231
  }
218
- async function clearRunArtifactRoots(worktreePath, request) {
232
+ export async function clearRunArtifactRoots(worktreePath, request) {
219
233
  const roots = new Set();
220
234
  roots.add(workItemArtifactRoot(worktreePath, request.workItem.id));
235
+ for (const artifact of request.priorPhaseArtifacts ?? []) {
236
+ roots.add(workItemArtifactRoot(worktreePath, artifact.workItemId));
237
+ }
221
238
  const ownerType = normalizeOwnerType(request.ownerType);
222
239
  const ownerId = request.ownerId?.trim();
223
240
  if (ownerType && ownerId) {
@@ -1,4 +1,4 @@
1
- import type { DispatchQaReworkContext, DispatchRepo, DispatchWorkItem, DispatchResolvedArtifact } from "../session/session.types.js";
1
+ import type { DispatchQaReworkContext, DispatchPriorPhaseArtifact, DispatchRepo, DispatchWorkItem, DispatchResolvedArtifact } from "../session/session.types.js";
2
2
  import type { WarmDependenciesResult } from "./dependency-warmer.js";
3
3
  export interface WorktreePrepareRequest {
4
4
  readonly repo: DispatchRepo;
@@ -12,6 +12,7 @@ export interface WorktreePrepareRequest {
12
12
  readonly allowedRepoRoot: string;
13
13
  readonly cleanupWorktrees: boolean;
14
14
  readonly artifacts: readonly DispatchResolvedArtifact[];
15
+ readonly priorPhaseArtifacts?: readonly DispatchPriorPhaseArtifact[];
15
16
  readonly qaRework?: DispatchQaReworkContext | null;
16
17
  readonly warmPathOverrides?: readonly string[];
17
18
  }
@@ -23,4 +24,5 @@ export interface WorktreeHandle {
23
24
  readonly cleanupWorktrees: boolean;
24
25
  readonly dependencyWarm?: WarmDependenciesResult;
25
26
  readonly qaReworkPacketPath?: string;
27
+ readonly hydratedPriorPhaseArtifactCount?: number;
26
28
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.3-dev.b3bce6ba65a6",
3
+ "version": "0.1.3-dev.ba7d36726eda",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
+ "forge": "dist/cli/bin.js",
8
9
  "forge-bridge": "dist/cli/bin.js"
9
10
  },
10
11
  "main": "./dist/index.js",
@@ -18,6 +19,7 @@
18
19
  },
19
20
  "files": [
20
21
  "dist",
22
+ "runtime",
21
23
  "README.md"
22
24
  ],
23
25
  "repository": {
@@ -40,10 +42,11 @@
40
42
  "test": "vitest run",
41
43
  "test:watch": "vitest",
42
44
  "smoke:package": "npm run build && node scripts/smoke-package.mjs",
43
- "lint": "eslint \"src/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\""
45
+ "lint": "eslint \"src/**/*.ts\" \"scripts/**/*.mjs\" \"vitest.config.ts\" \"vitest.setup.ts\""
44
46
  },
45
47
  "dependencies": {
46
- "@microsoft/signalr": "^8.0.17"
48
+ "@microsoft/signalr": "^8.0.17",
49
+ "yaml": "^2.9.0"
47
50
  },
48
51
  "devDependencies": {
49
52
  "@eslint/js": "^10.0.1",
@@ -0,0 +1,287 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [ValidateSet("Outer", "Inner")]
4
+ [string]$Mode,
5
+
6
+ [Parameter(Mandatory = $true)]
7
+ [string]$Payload,
8
+
9
+ [string]$GateName
10
+ )
11
+
12
+ $ErrorActionPreference = "Stop"
13
+
14
+ Add-Type -TypeDefinition @"
15
+ using System;
16
+ using System.Runtime.InteropServices;
17
+ using System.Text;
18
+
19
+ public static class ForgeWindowsJob
20
+ {
21
+ [StructLayout(LayoutKind.Sequential)]
22
+ private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
23
+ {
24
+ public long PerProcessUserTimeLimit;
25
+ public long PerJobUserTimeLimit;
26
+ public uint LimitFlags;
27
+ public UIntPtr MinimumWorkingSetSize;
28
+ public UIntPtr MaximumWorkingSetSize;
29
+ public uint ActiveProcessLimit;
30
+ public UIntPtr Affinity;
31
+ public uint PriorityClass;
32
+ public uint SchedulingClass;
33
+ }
34
+
35
+ [StructLayout(LayoutKind.Sequential)]
36
+ private struct IO_COUNTERS
37
+ {
38
+ public ulong ReadOperationCount;
39
+ public ulong WriteOperationCount;
40
+ public ulong OtherOperationCount;
41
+ public ulong ReadTransferCount;
42
+ public ulong WriteTransferCount;
43
+ public ulong OtherTransferCount;
44
+ }
45
+
46
+ [StructLayout(LayoutKind.Sequential)]
47
+ private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
48
+ {
49
+ public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
50
+ public IO_COUNTERS IoInfo;
51
+ public UIntPtr ProcessMemoryLimit;
52
+ public UIntPtr JobMemoryLimit;
53
+ public UIntPtr PeakProcessMemoryUsed;
54
+ public UIntPtr PeakJobMemoryUsed;
55
+ }
56
+
57
+ private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
58
+ private const int JobObjectExtendedLimitInformation = 9;
59
+
60
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
61
+ private static extern IntPtr CreateJobObject(IntPtr securityAttributes, string name);
62
+
63
+ [DllImport("kernel32.dll", SetLastError = true)]
64
+ private static extern bool SetInformationJobObject(
65
+ IntPtr job,
66
+ int informationClass,
67
+ IntPtr information,
68
+ uint informationLength);
69
+
70
+ [DllImport("kernel32.dll", SetLastError = true)]
71
+ public static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
72
+
73
+ [DllImport("kernel32.dll", SetLastError = true)]
74
+ public static extern bool TerminateJobObject(IntPtr job, uint exitCode);
75
+
76
+ [DllImport("kernel32.dll", SetLastError = true)]
77
+ public static extern bool CloseHandle(IntPtr handle);
78
+
79
+ public static IntPtr CreateKillOnCloseJob(string name)
80
+ {
81
+ IntPtr job = CreateJobObject(IntPtr.Zero, name);
82
+ if (job == IntPtr.Zero)
83
+ {
84
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObject failed.");
85
+ }
86
+
87
+ var limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
88
+ limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
89
+ int size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
90
+ IntPtr pointer = Marshal.AllocHGlobal(size);
91
+ try
92
+ {
93
+ Marshal.StructureToPtr(limits, pointer, false);
94
+ if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, pointer, (uint)size))
95
+ {
96
+ throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "SetInformationJobObject failed.");
97
+ }
98
+ }
99
+ catch
100
+ {
101
+ CloseHandle(job);
102
+ throw;
103
+ }
104
+ finally
105
+ {
106
+ Marshal.FreeHGlobal(pointer);
107
+ }
108
+
109
+ return job;
110
+ }
111
+
112
+ public static string BuildArguments(object[] arguments)
113
+ {
114
+ var result = new StringBuilder();
115
+ for (int index = 0; index < arguments.Length; index++)
116
+ {
117
+ if (index > 0) result.Append(' ');
118
+ result.Append(QuoteArgument(Convert.ToString(arguments[index]) ?? String.Empty));
119
+ }
120
+ return result.ToString();
121
+ }
122
+
123
+ private static string QuoteArgument(string argument)
124
+ {
125
+ if (argument.Length > 0 && argument.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0)
126
+ {
127
+ return argument;
128
+ }
129
+
130
+ var result = new StringBuilder();
131
+ result.Append('"');
132
+ int backslashes = 0;
133
+ foreach (char character in argument)
134
+ {
135
+ if (character == '\\')
136
+ {
137
+ backslashes++;
138
+ continue;
139
+ }
140
+
141
+ if (character == '"')
142
+ {
143
+ result.Append('\\', backslashes * 2 + 1);
144
+ result.Append('"');
145
+ backslashes = 0;
146
+ continue;
147
+ }
148
+
149
+ result.Append('\\', backslashes);
150
+ backslashes = 0;
151
+ result.Append(character);
152
+ }
153
+
154
+ result.Append('\\', backslashes * 2);
155
+ result.Append('"');
156
+ return result.ToString();
157
+ }
158
+ }
159
+ "@
160
+
161
+ function Read-ForgeSupervisorPayload {
162
+ $bytes = [Convert]::FromBase64String($Payload)
163
+ $json = [Text.Encoding]::UTF8.GetString($bytes)
164
+ return $json | ConvertFrom-Json
165
+ }
166
+
167
+ function New-ForgeChildProcess(
168
+ [string]$command,
169
+ [object[]]$arguments,
170
+ [string]$workingDirectory,
171
+ [bool]$redirectStreams
172
+ ) {
173
+ $startInfo = New-Object System.Diagnostics.ProcessStartInfo
174
+ $startInfo.FileName = $command
175
+ $startInfo.Arguments = [ForgeWindowsJob]::BuildArguments($arguments)
176
+ $startInfo.WorkingDirectory = $workingDirectory
177
+ $startInfo.UseShellExecute = $false
178
+ $startInfo.CreateNoWindow = $true
179
+ $startInfo.RedirectStandardInput = $redirectStreams
180
+ $startInfo.RedirectStandardOutput = $redirectStreams
181
+ $startInfo.RedirectStandardError = $redirectStreams
182
+
183
+ $child = New-Object System.Diagnostics.Process
184
+ $child.StartInfo = $startInfo
185
+ if (-not $child.Start()) {
186
+ throw "Failed to start supervised process '$command'."
187
+ }
188
+ return $child
189
+ }
190
+
191
+ function Start-ForgeOutputCopies([Diagnostics.Process]$child) {
192
+ return @(
193
+ $child.StandardOutput.BaseStream.CopyToAsync([Console]::OpenStandardOutput()),
194
+ $child.StandardError.BaseStream.CopyToAsync([Console]::OpenStandardError())
195
+ )
196
+ }
197
+
198
+ function Write-ForgeStandardInput([Diagnostics.Process]$child, [string]$inputText) {
199
+ if ($inputText.Length -gt 0) {
200
+ $child.StandardInput.Write($inputText)
201
+ }
202
+ $child.StandardInput.Close()
203
+ }
204
+
205
+ if ($Mode -eq "Inner") {
206
+ if ([string]::IsNullOrWhiteSpace($GateName)) {
207
+ throw "The inner supervisor requires a gate name."
208
+ }
209
+
210
+ $inputText = [Console]::In.ReadToEnd()
211
+ $request = Read-ForgeSupervisorPayload
212
+ $gate = [Threading.EventWaitHandle]::OpenExisting($GateName)
213
+ try {
214
+ if (-not $gate.WaitOne(30000)) {
215
+ throw "Timed out waiting for Windows Job Object assignment."
216
+ }
217
+ }
218
+ finally {
219
+ $gate.Dispose()
220
+ }
221
+
222
+ $provider = New-ForgeChildProcess ([string]$request.command) @($request.args) ([string]$request.cwd) $true
223
+ $copies = Start-ForgeOutputCopies $provider
224
+ Write-ForgeStandardInput $provider $inputText
225
+ $provider.WaitForExit()
226
+ if (-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]$copies, 1000)) {
227
+ $provider.StandardOutput.Dispose()
228
+ $provider.StandardError.Dispose()
229
+ }
230
+ exit $provider.ExitCode
231
+ }
232
+
233
+ $inputText = [Console]::In.ReadToEnd()
234
+ $request = Read-ForgeSupervisorPayload
235
+ $parent = [Diagnostics.Process]::GetProcessById([int]$request.parentPid)
236
+ $jobName = "Local\Forge-$([string]$request.supervisorId)"
237
+ $job = [ForgeWindowsJob]::CreateKillOnCloseJob($jobName)
238
+ $gateNameValue = "Local\ForgeGate-$([Guid]::NewGuid().ToString('N'))"
239
+ $createdNew = $false
240
+ $gate = New-Object Threading.EventWaitHandle($false, [Threading.EventResetMode]::ManualReset, $gateNameValue, [ref]$createdNew)
241
+ $inner = $null
242
+
243
+ try {
244
+ $powerShell = Join-Path $PSHOME "powershell.exe"
245
+ $innerArguments = @(
246
+ "-NoLogo",
247
+ "-NoProfile",
248
+ "-NonInteractive",
249
+ "-ExecutionPolicy",
250
+ "Bypass",
251
+ "-File",
252
+ $PSCommandPath,
253
+ "-Mode",
254
+ "Inner",
255
+ "-Payload",
256
+ $Payload,
257
+ "-GateName",
258
+ $gateNameValue
259
+ )
260
+ $inner = New-ForgeChildProcess $powerShell $innerArguments ([string]$request.cwd) $true
261
+ if (-not [ForgeWindowsJob]::AssignProcessToJobObject($job, $inner.Handle)) {
262
+ throw (New-Object System.ComponentModel.Win32Exception([Runtime.InteropServices.Marshal]::GetLastWin32Error(), "AssignProcessToJobObject failed."))
263
+ }
264
+
265
+ $copies = Start-ForgeOutputCopies $inner
266
+ $gate.Set() | Out-Null
267
+ Write-ForgeStandardInput $inner $inputText
268
+ while (-not $inner.WaitForExit(250)) {
269
+ $parent.Refresh()
270
+ if ($parent.HasExited) {
271
+ [ForgeWindowsJob]::TerminateJobObject($job, 125) | Out-Null
272
+ exit 125
273
+ }
274
+ }
275
+
276
+ $exitCode = $inner.ExitCode
277
+ [ForgeWindowsJob]::TerminateJobObject($job, 1) | Out-Null
278
+ [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]$copies, 2000) | Out-Null
279
+ exit $exitCode
280
+ }
281
+ finally {
282
+ if ($inner -and -not $inner.HasExited) {
283
+ [ForgeWindowsJob]::TerminateJobObject($job, 1) | Out-Null
284
+ }
285
+ $gate.Dispose()
286
+ [ForgeWindowsJob]::CloseHandle($job) | Out-Null
287
+ }
@@ -0,0 +1,33 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [int]$RootProcessId,
4
+ [Parameter(Mandatory = $true)]
5
+ [string]$ListenerProcessIds
6
+ )
7
+
8
+ $ErrorActionPreference = "Stop"
9
+ $parents = @{}
10
+ Get-CimInstance Win32_Process | ForEach-Object {
11
+ $parents[[int]$_.ProcessId] = [int]$_.ParentProcessId
12
+ }
13
+
14
+ $results = @()
15
+ foreach ($listenerText in $ListenerProcessIds.Split(',')) {
16
+ $current = [int]$listenerText
17
+ $visited = @{}
18
+ $owned = $false
19
+ while ($current -gt 0 -and -not $visited.ContainsKey($current)) {
20
+ if ($current -eq $RootProcessId) {
21
+ $owned = $true
22
+ break
23
+ }
24
+ $visited[$current] = $true
25
+ if (-not $parents.ContainsKey($current)) {
26
+ break
27
+ }
28
+ $current = [int]$parents[$current]
29
+ }
30
+ $results += $owned
31
+ }
32
+
33
+ ConvertTo-Json -Compress -InputObject @($results)