@abelxiaoxing/cadence 1.4.1 → 1.5.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.
@@ -1,26 +1,20 @@
1
- import {
2
- lstatSync,
3
- mkdirSync,
4
- mkdtempSync,
5
- readdirSync,
6
- readlinkSync,
7
- realpathSync,
8
- rmSync,
9
- symlinkSync,
10
- writeFileSync,
11
- } from "node:fs";
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { mkdtemp } from "node:fs/promises";
12
4
  import path from "node:path";
13
5
  import {
14
6
  type StructuredVerificationContract,
15
7
  VERIFICATION_CONFIGURATION_PATHS,
16
8
  } from "./contracts.ts";
17
- import type { IsolationMount } from "./isolation-backend.ts";
9
+ import { executionProfile } from "./execution-profile.ts";
18
10
  import {
19
11
  resolveVerificationRunner,
20
12
  type VerificationRunnerBinding,
21
13
  validateVerificationAdapterCapability,
22
14
  verificationRunnerFiles,
23
15
  } from "./verification-capability.ts";
16
+ import type { prepareVerificationEnvironmentIo } from "./verification-environment-io.ts";
17
+ import type { WorkspaceIoMetrics } from "./workspace-io.ts";
24
18
  import { runWorkspaceIo } from "./workspace-io.ts";
25
19
 
26
20
  export async function captureVerificationEnvironmentIdentity(
@@ -49,165 +43,73 @@ export async function captureVerificationEnvironmentIdentity(
49
43
  for (const binding of capability.runnerBindings)
50
44
  if (binding.mountSource) roots.push(binding.mountSource);
51
45
  }
52
- return runWorkspaceIo<string>({
46
+ const installed = await runWorkspaceIo<string>({
53
47
  root: dependencyOwner,
54
48
  artifactRoot: dependencyOwner,
55
49
  operation: "verificationIdentity",
56
50
  args: [roots],
57
51
  signal,
58
52
  });
53
+ const profile = executionProfile();
54
+ return createHash("sha256")
55
+ .update(installed)
56
+ .update(JSON.stringify(profile))
57
+ .update(
58
+ JSON.stringify(
59
+ profile.inheritEnvironment.map((name) => [
60
+ name,
61
+ process.env[name] ?? null,
62
+ ]),
63
+ ),
64
+ )
65
+ .digest("hex");
59
66
  }
60
67
 
61
- const CACHES = [".vite", ".vite-temp"];
62
- function within(root: string, candidate: string): boolean {
63
- const relative = path.relative(root, candidate);
64
- return (
65
- relative !== ".." &&
66
- !relative.startsWith(`..${path.sep}`) &&
67
- !path.isAbsolute(relative)
68
- );
69
- }
70
-
71
- /** A disposable dependency view: immutable packages and independently writable caches. */
72
- export function prepareVerificationEnvironment(
68
+ /** All dependency traversal/copy/removal runs outside the host event loop. */
69
+ export async function prepareVerificationEnvironment(
73
70
  root: string,
74
71
  dependencyOwner: string,
75
72
  runnerBindings: readonly VerificationRunnerBinding[],
73
+ signal?: AbortSignal,
74
+ onMetrics?: (metrics: WorkspaceIoMetrics) => void,
76
75
  ) {
77
- const privateRoot = mkdtempSync(
76
+ signal?.throwIfAborted();
77
+ const profile = executionProfile();
78
+ const privateRoot = await mkdtemp(
78
79
  path.join(path.dirname(root), ".cadence-verification-"),
79
80
  );
80
- const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
81
- try {
82
- const mounts: IsolationMount[] = [
83
- { source: privateRoot, target: "/cadence", writable: true },
84
- ];
85
- for (const directory of [
86
- "home",
87
- "cache",
88
- "tmp",
89
- "reports",
90
- "dependencies",
91
- ]) {
92
- mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
93
- }
94
- writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
95
- writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
96
- const dependencies = path.join(dependencyOwner, "node_modules");
97
- const stat = lstatSync(dependencies, { throwIfNoEntry: false });
98
- if (stat) {
99
- const target = path.join(root, "node_modules");
100
- const targetStat = lstatSync(target, { throwIfNoEntry: false });
101
- if (
102
- !stat.isDirectory() ||
103
- stat.isSymbolicLink() ||
104
- (targetStat &&
105
- (!targetStat.isDirectory() || targetStat.isSymbolicLink()))
106
- ) {
107
- throw new Error("dependency-path-unsafe");
108
- }
109
- if (!targetStat) mkdirSync(target, { mode: 0o700 });
110
- const view = path.join(privateRoot, "dependencies");
111
- mounts.push({ source: view, target: "/workspace/node_modules" });
112
- const entries = readdirSync(dependencies);
113
- if (entries.length > 20_000) throw new Error("dependency-view-too-large");
114
- const canonical = realpathSync(dependencies);
115
- for (const name of entries) {
116
- if (CACHES.includes(name)) continue;
117
- const source = path.join(dependencies, name);
118
- const placeholder = path.join(view, name);
119
- const entry = lstatSync(source);
120
- if (entry.isSymbolicLink()) {
121
- const link = readlinkSync(source);
122
- if (
123
- path.isAbsolute(link) ||
124
- !within(canonical, realpathSync(source))
125
- ) {
126
- throw new Error("dependency-path-unsafe");
127
- }
128
- symlinkSync(link, placeholder);
129
- } else {
130
- if (entry.isDirectory()) mkdirSync(placeholder);
131
- else if (entry.isFile()) writeFileSync(placeholder, "");
132
- else throw new Error("dependency-path-unsafe");
133
- mounts.push({ source, target: `/workspace/node_modules/${name}` });
134
- }
135
- }
136
- for (const cache of CACHES) {
137
- mkdirSync(path.join(view, cache));
138
- const source = path.join(privateRoot, "cache", cache);
139
- mkdirSync(source);
140
- mounts.push({
141
- source,
142
- target: `/workspace/node_modules/${cache}`,
143
- writable: true,
144
- });
145
- }
146
- }
147
- const sources = new Map<string, string>();
148
- const bindings = runnerBindings.map((binding) => {
149
- if (!binding.mountSource) return { ...binding };
150
- const source = path.resolve(binding.mountSource);
151
- const stat = lstatSync(source);
152
- const executable = realpathSync(binding.executablePath);
153
- if (
154
- !stat.isDirectory() ||
155
- stat.isSymbolicLink() ||
156
- !within(source, executable)
157
- ) {
158
- throw new Error("runner-mount-unavailable");
159
- }
160
- let target = sources.get(source);
161
- if (!target) {
162
- target = `/cadence-runners/${sources.size}`;
163
- sources.set(source, target);
164
- mounts.push({ source, target });
165
- }
166
- const translate = (file: string) =>
167
- path.posix.join(target, ...path.relative(source, file).split(path.sep));
168
- return {
169
- ...binding,
170
- executablePath: translate(executable),
171
- ...(binding.fixedArgs
172
- ? {
173
- fixedArgs: binding.fixedArgs.map((arg) =>
174
- path.isAbsolute(arg) && within(source, arg)
175
- ? translate(arg)
176
- : arg,
177
- ),
178
- }
179
- : {}),
180
- };
81
+ const cleanup = () =>
82
+ runWorkspaceIo<void>({
83
+ root,
84
+ artifactRoot: root,
85
+ operation: "cleanupVerification",
86
+ args: [privateRoot],
181
87
  });
182
- return {
183
- privateRoot,
184
- mounts,
185
- bindings,
186
- cleanup,
187
- environment: {
188
- CI: "1",
189
- HOME: "/cadence/home",
190
- XDG_CACHE_HOME: "/cadence/cache",
191
- TMPDIR: "/cadence/tmp",
192
- npm_config_cache: "/cadence/cache/npm",
193
- npm_config_userconfig: "/cadence/home/user.npmrc",
194
- npm_config_globalconfig: "/cadence/home/global.npmrc",
195
- PATH: [
196
- ...new Set(
197
- // Honor explicitly resolved private runtimes before system fallbacks.
198
- [
199
- ...bindings.filter((binding) => binding.mountSource),
200
- ...bindings.filter((binding) => !binding.mountSource),
201
- ].map((binding) => path.posix.dirname(binding.executablePath)),
88
+ try {
89
+ const prepared = await runWorkspaceIo<
90
+ ReturnType<typeof prepareVerificationEnvironmentIo>
91
+ >({
92
+ root,
93
+ artifactRoot: root,
94
+ operation: "prepareVerification",
95
+ args: [
96
+ root,
97
+ dependencyOwner,
98
+ runnerBindings,
99
+ profile,
100
+ Object.fromEntries(
101
+ ["PATH", ...profile.inheritEnvironment].flatMap((name) =>
102
+ process.env[name] === undefined ? [] : [[name, process.env[name]]],
202
103
  ),
203
- "/usr/local/bin",
204
- "/usr/bin",
205
- "/bin",
206
- ].join(":"),
207
- },
208
- };
104
+ ),
105
+ privateRoot,
106
+ ],
107
+ signal,
108
+ onMetrics,
109
+ });
110
+ return { ...prepared, cleanup };
209
111
  } catch (error) {
210
- cleanup();
112
+ await cleanup();
211
113
  throw error;
212
114
  }
213
115
  }
@@ -0,0 +1,64 @@
1
+ /** Only callers admitted before I/O begins share a scan. A later currentness check
2
+ * must observe bytes after its own admission, not join an older in-flight scan.
3
+ * Last cancellation drains
4
+ * the worker before returning; a cancelled scan never admits a new subscriber. */
5
+ export function coalesceVerificationScans<P>(
6
+ scan: (plan: P, signal: AbortSignal) => Promise<string>,
7
+ ) {
8
+ type Pending = {
9
+ controller: AbortController;
10
+ promise: Promise<string>;
11
+ subscribers: number;
12
+ started: boolean;
13
+ };
14
+ const pending = new Map<string, Pending>();
15
+ return (plan: P, signal?: AbortSignal): Promise<string> => {
16
+ signal?.throwIfAborted();
17
+ const key = JSON.stringify(plan);
18
+ let entry = pending.get(key);
19
+ if (!entry || entry.started || entry.controller.signal.aborted) {
20
+ const controller = new AbortController();
21
+ const created: Pending = {
22
+ controller,
23
+ subscribers: 0,
24
+ started: false,
25
+ promise: Promise.resolve(""),
26
+ };
27
+ created.promise = Promise.resolve()
28
+ .then(() => {
29
+ created.started = true;
30
+ return scan(plan, controller.signal);
31
+ })
32
+ .finally(() => {
33
+ if (pending.get(key) === created) pending.delete(key);
34
+ });
35
+ entry = created;
36
+ pending.set(key, entry);
37
+ }
38
+ const shared = entry;
39
+ shared.subscribers++;
40
+ return new Promise((resolve, reject) => {
41
+ let cancelled = false;
42
+ const abort = () => {
43
+ if (cancelled) return;
44
+ cancelled = true;
45
+ if (--shared.subscribers === 0) shared.controller.abort(signal?.reason);
46
+ };
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ if (signal?.aborted) abort();
49
+ const settle = (value?: string, error?: unknown) => {
50
+ signal?.removeEventListener("abort", abort);
51
+ if (!cancelled) shared.subscribers--;
52
+ if (cancelled)
53
+ reject(signal?.reason ?? new DOMException("cancelled", "AbortError"));
54
+ else if (error !== undefined) reject(error);
55
+ else if (value !== undefined) resolve(value);
56
+ else reject(new Error("verification-scan-result-unavailable"));
57
+ };
58
+ shared.promise.then(
59
+ (value) => settle(value),
60
+ (error) => settle(undefined, error),
61
+ );
62
+ });
63
+ };
64
+ }
@@ -178,6 +178,8 @@ function capable(
178
178
  function safeFailureCode(error: unknown): string {
179
179
  if (error instanceof RouteAttemptStop || error instanceof TransportTimeout)
180
180
  return error.code;
181
+ if (error instanceof Error && error.message === "child-provider-rate-limited")
182
+ return error.message;
181
183
  return "transport-failure";
182
184
  }
183
185
 
@@ -1,5 +1,7 @@
1
+ import { rmSync } from "node:fs";
1
2
  import { parentPort, workerData } from "node:worker_threads";
2
3
  import { ArtifactStore } from "./artifact-store.ts";
4
+ import { prepareVerificationEnvironmentIo } from "./verification-environment-io.ts";
3
5
  import { verificationEnvironmentDigest } from "./verification-identity.ts";
4
6
  import { WorkspaceStore } from "./workspace-store.ts";
5
7
 
@@ -10,7 +12,23 @@ const checkCancelled = () => {
10
12
  if (Atomics.load(flag, 0)) throw new Error("cancelled");
11
13
  };
12
14
  try {
13
- if (workerData.operation === "verificationIdentity") {
15
+ if (workerData.operation === "prepareVerification") {
16
+ const [root, owner, bindings, profile, environment, scratch] =
17
+ workerData.args;
18
+ const result = prepareVerificationEnvironmentIo(
19
+ root,
20
+ owner,
21
+ bindings,
22
+ profile,
23
+ environment,
24
+ checkCancelled,
25
+ scratch,
26
+ );
27
+ parentPort.postMessage({ ok: true, result, files: 0, bytes: 0 });
28
+ } else if (workerData.operation === "cleanupVerification") {
29
+ rmSync(workerData.args[0], { recursive: true, force: true });
30
+ parentPort.postMessage({ ok: true, files: 0, bytes: 0 });
31
+ } else if (workerData.operation === "verificationIdentity") {
14
32
  const result = verificationEnvironmentDigest(
15
33
  workerData.args[0],
16
34
  checkCancelled,