@visulima/task-runner 0.0.1 → 1.0.0-alpha.2
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 +21 -0
- package/LICENSE.md +21 -0
- package/README.md +170 -36
- package/binding.js +204 -0
- package/dist/affected.d.ts +48 -0
- package/dist/cache.d.ts +103 -0
- package/dist/default-task-runner.d.ts +44 -0
- package/dist/file-access-tracker.d.ts +53 -0
- package/dist/fingerprint.d.ts +45 -0
- package/dist/framework-inference.d.ts +35 -0
- package/dist/graph-visualizer.d.ts +74 -0
- package/dist/incremental-hasher.d.ts +58 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +20 -0
- package/dist/life-cycle.d.ts +36 -0
- package/dist/lockfile-hasher.d.ts +73 -0
- package/dist/native-binding.d.ts +64 -0
- package/dist/packem_shared/Cache-IYpTYVUC.js +298 -0
- package/dist/packem_shared/CompositeLifeCycle-7AtYw1dv.js +112 -0
- package/dist/packem_shared/FileAccessTracker-CrtBAt5D.js +239 -0
- package/dist/packem_shared/FingerprintManager-D6Y0erg-.js +227 -0
- package/dist/packem_shared/IncrementalFileHasher-Ds3J6dgb.js +151 -0
- package/dist/packem_shared/RemoteCache-BDqrnDEi.js +179 -0
- package/dist/packem_shared/TaskOrchestrator-BvYs3ONw.js +342 -0
- package/dist/packem_shared/TaskScheduler-CJilHDta.js +111 -0
- package/dist/packem_shared/TrackedTaskExecutor-BGUKFE-7.js +164 -0
- package/dist/packem_shared/collectFiles-ClXHnHhg.js +22 -0
- package/dist/packem_shared/computeTaskHash-BoCnnvIJ.js +356 -0
- package/dist/packem_shared/createTaskGraph-CcsFaSrz.js +164 -0
- package/dist/packem_shared/defaultTaskRunner-CrW4v5Ye.js +79 -0
- package/dist/packem_shared/detectFrameworks-CeFzKE6J.js +101 -0
- package/dist/packem_shared/extractPackageName-CbVNW-dr.js +189 -0
- package/dist/packem_shared/filterAffectedTasks-I-18zPg6.js +135 -0
- package/dist/packem_shared/findCycle-DF4_BRdO.js +212 -0
- package/dist/packem_shared/generateRunSummary-qn-_jKwt.js +134 -0
- package/dist/packem_shared/isNativeAvailable-BWhnZ4ES.js +19 -0
- package/dist/packem_shared/projectGraphToDot-VdTjHcVp.js +202 -0
- package/dist/packem_shared/utils-zO0ZRgtf.js +390 -0
- package/dist/remote-cache.d.ts +55 -0
- package/dist/run-summary.d.ts +89 -0
- package/dist/task-graph-utils.d.ts +39 -0
- package/dist/task-graph.d.ts +22 -0
- package/dist/task-hasher.d.ts +67 -0
- package/dist/task-orchestrator.d.ts +38 -0
- package/dist/task-scheduler.d.ts +18 -0
- package/dist/tracked-executor.d.ts +46 -0
- package/dist/types.d.ts +385 -0
- package/dist/utils.d.ts +39 -0
- package/package.json +72 -7
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { d as resolveTaskCwd, a as createFailureResult, e as createXxh3Hasher } from './utils-zO0ZRgtf.js';
|
|
2
|
+
import { join } from '@visulima/path';
|
|
3
|
+
import { FingerprintManager } from './FingerprintManager-D6Y0erg-.js';
|
|
4
|
+
import { generateRunSummary, writeRunSummary } from './generateRunSummary-qn-_jKwt.js';
|
|
5
|
+
import { computeTaskHash } from './computeTaskHash-BoCnnvIJ.js';
|
|
6
|
+
import { TrackedTaskExecutor } from './TrackedTaskExecutor-BGUKFE-7.js';
|
|
7
|
+
|
|
8
|
+
const hashFingerprint = (fingerprint) => {
|
|
9
|
+
const hash = createXxh3Hasher();
|
|
10
|
+
hash.update(fingerprint.commandHash);
|
|
11
|
+
for (const key of Object.keys(fingerprint.fileHashes).toSorted()) {
|
|
12
|
+
hash.update(key);
|
|
13
|
+
hash.update(fingerprint.fileHashes[key]);
|
|
14
|
+
}
|
|
15
|
+
for (const path of fingerprint.missingFiles) {
|
|
16
|
+
hash.update(`missing:${path}`);
|
|
17
|
+
}
|
|
18
|
+
for (const key of Object.keys(fingerprint.directoryListings).toSorted()) {
|
|
19
|
+
hash.update(`dir:${key}`);
|
|
20
|
+
hash.update(JSON.stringify(fingerprint.directoryListings[key]));
|
|
21
|
+
}
|
|
22
|
+
for (const key of Object.keys(fingerprint.envHashes).toSorted()) {
|
|
23
|
+
hash.update(key);
|
|
24
|
+
hash.update(fingerprint.envHashes[key]);
|
|
25
|
+
}
|
|
26
|
+
return hash.digest();
|
|
27
|
+
};
|
|
28
|
+
const createDeferred = () => {
|
|
29
|
+
let resolve;
|
|
30
|
+
const promise = new Promise((r) => {
|
|
31
|
+
resolve = r;
|
|
32
|
+
});
|
|
33
|
+
return { promise, resolve };
|
|
34
|
+
};
|
|
35
|
+
class TaskOrchestrator {
|
|
36
|
+
#taskHasher;
|
|
37
|
+
#cache;
|
|
38
|
+
#scheduler;
|
|
39
|
+
#lifeCycle;
|
|
40
|
+
#taskExecutor;
|
|
41
|
+
#workspaceRoot;
|
|
42
|
+
#skipCache;
|
|
43
|
+
#captureOutput;
|
|
44
|
+
#autoFingerprint;
|
|
45
|
+
#fingerprintManager;
|
|
46
|
+
#trackedExecutor;
|
|
47
|
+
#fingerprintEnvPatterns;
|
|
48
|
+
#untrackedEnvVars;
|
|
49
|
+
#cacheDiagnostics;
|
|
50
|
+
#resolveCommand;
|
|
51
|
+
#remoteCache;
|
|
52
|
+
#dryRun;
|
|
53
|
+
#summarize;
|
|
54
|
+
#taskGraph;
|
|
55
|
+
#results = /* @__PURE__ */ new Map();
|
|
56
|
+
#startTime;
|
|
57
|
+
/** Tracks in-flight task promises so the execution loop can await them */
|
|
58
|
+
#inFlightTasks = /* @__PURE__ */ new Map();
|
|
59
|
+
/** Deferred that gets resolved whenever a task completes, waking the loop */
|
|
60
|
+
#taskCompletionSignal = createDeferred();
|
|
61
|
+
#aborted = false;
|
|
62
|
+
constructor(options) {
|
|
63
|
+
this.#taskHasher = options.taskHasher;
|
|
64
|
+
this.#cache = options.cache;
|
|
65
|
+
this.#scheduler = options.scheduler;
|
|
66
|
+
this.#lifeCycle = options.lifeCycle;
|
|
67
|
+
this.#taskExecutor = options.taskExecutor;
|
|
68
|
+
this.#workspaceRoot = options.workspaceRoot;
|
|
69
|
+
this.#skipCache = options.skipCache ?? false;
|
|
70
|
+
this.#captureOutput = options.captureOutput ?? true;
|
|
71
|
+
this.#autoFingerprint = options.autoFingerprint ?? false;
|
|
72
|
+
this.#fingerprintEnvPatterns = options.fingerprintEnvPatterns ?? [];
|
|
73
|
+
this.#untrackedEnvVars = options.untrackedEnvVars ?? [];
|
|
74
|
+
this.#cacheDiagnostics = options.cacheDiagnostics ?? false;
|
|
75
|
+
this.#resolveCommand = options.resolveCommand ?? void 0;
|
|
76
|
+
this.#remoteCache = options.remoteCache ?? void 0;
|
|
77
|
+
this.#dryRun = options.dryRun ?? false;
|
|
78
|
+
this.#summarize = options.summarize ?? false;
|
|
79
|
+
this.#taskGraph = options.taskGraph ?? void 0;
|
|
80
|
+
this.#startTime = Date.now();
|
|
81
|
+
if (this.#autoFingerprint) {
|
|
82
|
+
this.#fingerprintManager = new FingerprintManager(options.workspaceRoot);
|
|
83
|
+
this.#trackedExecutor = new TrackedTaskExecutor(options.workspaceRoot);
|
|
84
|
+
} else {
|
|
85
|
+
this.#fingerprintManager = void 0;
|
|
86
|
+
this.#trackedExecutor = void 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async run() {
|
|
90
|
+
this.#lifeCycle.startCommand?.();
|
|
91
|
+
const signalHandler = () => {
|
|
92
|
+
this.#aborted = true;
|
|
93
|
+
this.#trackedExecutor?.killAll();
|
|
94
|
+
};
|
|
95
|
+
process.on("SIGINT", signalHandler);
|
|
96
|
+
process.on("SIGTERM", signalHandler);
|
|
97
|
+
try {
|
|
98
|
+
await this.#executionLoop();
|
|
99
|
+
} finally {
|
|
100
|
+
process.removeListener("SIGINT", signalHandler);
|
|
101
|
+
process.removeListener("SIGTERM", signalHandler);
|
|
102
|
+
this.#lifeCycle.endCommand?.();
|
|
103
|
+
}
|
|
104
|
+
if (this.#summarize && this.#taskGraph) {
|
|
105
|
+
const summary = generateRunSummary(this.#results, this.#taskGraph, this.#startTime);
|
|
106
|
+
await writeRunSummary(summary, this.#workspaceRoot);
|
|
107
|
+
}
|
|
108
|
+
return this.#results;
|
|
109
|
+
}
|
|
110
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
111
|
+
async #executionLoop() {
|
|
112
|
+
while (!this.#scheduler.isComplete() && !this.#aborted) {
|
|
113
|
+
const batch = this.#scheduler.getNextBatch();
|
|
114
|
+
if (batch.length === 0) {
|
|
115
|
+
if (this.#inFlightTasks.size > 0) {
|
|
116
|
+
await this.#taskCompletionSignal.promise;
|
|
117
|
+
this.#taskCompletionSignal = createDeferred();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (this.#scheduler.remainingCount > 0) {
|
|
121
|
+
throw new Error("Deadlock detected: tasks remain but none can be scheduled. This may indicate a circular dependency.");
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
for (const task of batch) {
|
|
126
|
+
this.#lifeCycle.scheduleTask?.(task);
|
|
127
|
+
this.#scheduler.startTask(task.id);
|
|
128
|
+
}
|
|
129
|
+
this.#lifeCycle.startTasks?.(batch);
|
|
130
|
+
for (const task of batch) {
|
|
131
|
+
const taskPromise = (this.#autoFingerprint ? this.#processTaskWithFingerprint(task) : this.#processTask(task)).catch((error) => {
|
|
132
|
+
const errorResult = createFailureResult(task, error, Date.now());
|
|
133
|
+
this.#results.set(task.id, errorResult);
|
|
134
|
+
return errorResult;
|
|
135
|
+
}).then((result) => {
|
|
136
|
+
this.#inFlightTasks.delete(task.id);
|
|
137
|
+
this.#scheduler.completeTask(task.id);
|
|
138
|
+
this.#lifeCycle.endTasks?.([result]);
|
|
139
|
+
if (result.terminalOutput) {
|
|
140
|
+
this.#lifeCycle.printTaskTerminalOutput?.(result.task, result.status, result.terminalOutput);
|
|
141
|
+
}
|
|
142
|
+
this.#taskCompletionSignal.resolve();
|
|
143
|
+
return result;
|
|
144
|
+
});
|
|
145
|
+
this.#inFlightTasks.set(task.id, taskPromise);
|
|
146
|
+
}
|
|
147
|
+
if (this.#inFlightTasks.size > 0) {
|
|
148
|
+
await this.#taskCompletionSignal.promise;
|
|
149
|
+
this.#taskCompletionSignal = createDeferred();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (this.#inFlightTasks.size > 0) {
|
|
153
|
+
await Promise.all(this.#inFlightTasks.values());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
157
|
+
async #processTask(task) {
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
const hashDetails = await this.#taskHasher.hashTask(task);
|
|
160
|
+
const hash = computeTaskHash(hashDetails);
|
|
161
|
+
Object.assign(task, { hash, hashDetails });
|
|
162
|
+
if (this.#dryRun) {
|
|
163
|
+
return this.#dryRunResult(task, startTime);
|
|
164
|
+
}
|
|
165
|
+
if (!this.#skipCache && task.cache !== false) {
|
|
166
|
+
const cachedResult = await this.#cache.get(hash);
|
|
167
|
+
if (cachedResult) {
|
|
168
|
+
return this.#applyCachedResult(task, cachedResult, startTime);
|
|
169
|
+
}
|
|
170
|
+
if (this.#remoteCache) {
|
|
171
|
+
const retrieved = await this.#remoteCache.retrieve(hash, this.#cache.cacheDirectory);
|
|
172
|
+
if (retrieved) {
|
|
173
|
+
const remoteCached = await this.#cache.get(hash);
|
|
174
|
+
if (remoteCached) {
|
|
175
|
+
const result2 = await this.#applyCachedResult(task, remoteCached, startTime);
|
|
176
|
+
result2.status = "remote-cache";
|
|
177
|
+
return result2;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const result = await this.#executeTask(task, startTime);
|
|
183
|
+
if (result.code === 0 && task.cache !== false && task.hash && this.#remoteCache) {
|
|
184
|
+
this.#remoteCache.store(task.hash, this.#cache.cacheDirectory).catch(() => {
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
190
|
+
async #processTaskWithFingerprint(task) {
|
|
191
|
+
const startTime = Date.now();
|
|
192
|
+
if (!this.#skipCache && task.cache !== false) {
|
|
193
|
+
const cachedResult = await this.#cache.getByTaskId(task.id);
|
|
194
|
+
if (cachedResult?.fingerprint && this.#fingerprintManager) {
|
|
195
|
+
const commandMiss = this.#fingerprintManager.validateCommand(
|
|
196
|
+
cachedResult.fingerprint,
|
|
197
|
+
`${task.target.project}:${task.target.target}`,
|
|
198
|
+
task.overrides
|
|
199
|
+
);
|
|
200
|
+
if (commandMiss) {
|
|
201
|
+
if (this.#cacheDiagnostics) {
|
|
202
|
+
this.#lifeCycle.printCacheMiss?.(task, this.#fingerprintManager.formatMissReasons([commandMiss]));
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const missReasons = await this.#fingerprintManager.validate(cachedResult.fingerprint);
|
|
206
|
+
if (!missReasons) {
|
|
207
|
+
return this.#applyCachedResult(task, cachedResult, startTime);
|
|
208
|
+
}
|
|
209
|
+
if (this.#cacheDiagnostics) {
|
|
210
|
+
this.#lifeCycle.printCacheMiss?.(task, this.#fingerprintManager.formatMissReasons(missReasons));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else if (this.#cacheDiagnostics && !cachedResult) {
|
|
214
|
+
this.#lifeCycle.printCacheMiss?.(task, "Cache miss reasons:\n - No previous fingerprint found (first run)");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return this.#executeTaskWithTracking(task, startTime);
|
|
218
|
+
}
|
|
219
|
+
async #applyCachedResult(task, cachedResult, startTime) {
|
|
220
|
+
const restored = await this.#cache.restoreOutputs(cachedResult.hash, task.outputs);
|
|
221
|
+
const status = restored ? "local-cache" : "local-cache-kept-existing";
|
|
222
|
+
const result = {
|
|
223
|
+
code: cachedResult.code,
|
|
224
|
+
endTime: Date.now(),
|
|
225
|
+
startTime,
|
|
226
|
+
status,
|
|
227
|
+
task,
|
|
228
|
+
terminalOutput: cachedResult.terminalOutput
|
|
229
|
+
};
|
|
230
|
+
this.#results.set(task.id, result);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
async #executeTask(task, startTime) {
|
|
234
|
+
try {
|
|
235
|
+
const { code, terminalOutput } = await this.#taskExecutor(task, {
|
|
236
|
+
captureOutput: this.#captureOutput,
|
|
237
|
+
cwd: resolveTaskCwd(this.#workspaceRoot, task)
|
|
238
|
+
});
|
|
239
|
+
const result = {
|
|
240
|
+
code,
|
|
241
|
+
endTime: Date.now(),
|
|
242
|
+
startTime,
|
|
243
|
+
status: code === 0 ? "success" : "failure",
|
|
244
|
+
task,
|
|
245
|
+
terminalOutput
|
|
246
|
+
};
|
|
247
|
+
this.#results.set(task.id, result);
|
|
248
|
+
if (code === 0 && task.cache !== false && task.hash) {
|
|
249
|
+
await this.#cache.put(task.hash, terminalOutput, task.outputs, code);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const result = createFailureResult(task, error, startTime);
|
|
254
|
+
this.#results.set(task.id, result);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async #executeTaskWithTracking(task, startTime) {
|
|
259
|
+
if (!this.#fingerprintManager) {
|
|
260
|
+
return this.#executeTask(task, startTime);
|
|
261
|
+
}
|
|
262
|
+
const taskCommand = `${task.target.project}:${task.target.target}`;
|
|
263
|
+
const cwd = resolveTaskCwd(this.#workspaceRoot, task);
|
|
264
|
+
try {
|
|
265
|
+
let code;
|
|
266
|
+
let terminalOutput;
|
|
267
|
+
let fingerprint;
|
|
268
|
+
const shellCommand = this.#resolveCommand?.(task);
|
|
269
|
+
const canTrack = shellCommand && this.#trackedExecutor?.isTrackingSupported;
|
|
270
|
+
if (canTrack && this.#trackedExecutor) {
|
|
271
|
+
const trackedResult = await this.#trackedExecutor.execute(task, { captureOutput: this.#captureOutput, cwd }, shellCommand);
|
|
272
|
+
code = trackedResult.code;
|
|
273
|
+
terminalOutput = trackedResult.terminalOutput;
|
|
274
|
+
fingerprint = await this.#fingerprintManager.createFingerprint(
|
|
275
|
+
trackedResult.accesses,
|
|
276
|
+
taskCommand,
|
|
277
|
+
task.overrides,
|
|
278
|
+
process.env,
|
|
279
|
+
this.#fingerprintEnvPatterns,
|
|
280
|
+
this.#untrackedEnvVars
|
|
281
|
+
);
|
|
282
|
+
} else {
|
|
283
|
+
const executionResult = await this.#taskExecutor(task, {
|
|
284
|
+
captureOutput: this.#captureOutput,
|
|
285
|
+
cwd
|
|
286
|
+
});
|
|
287
|
+
code = executionResult.code;
|
|
288
|
+
terminalOutput = executionResult.terminalOutput;
|
|
289
|
+
const hashDetails = await this.#taskHasher.hashTask(task);
|
|
290
|
+
const fileAccesses = Object.keys(hashDetails.nodes).map((filePath) => {
|
|
291
|
+
return {
|
|
292
|
+
path: join(this.#workspaceRoot, filePath),
|
|
293
|
+
type: "read"
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
fingerprint = await this.#fingerprintManager.createFingerprint(
|
|
297
|
+
fileAccesses,
|
|
298
|
+
taskCommand,
|
|
299
|
+
task.overrides,
|
|
300
|
+
process.env,
|
|
301
|
+
this.#fingerprintEnvPatterns,
|
|
302
|
+
this.#untrackedEnvVars
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const result = {
|
|
306
|
+
code,
|
|
307
|
+
endTime: Date.now(),
|
|
308
|
+
startTime,
|
|
309
|
+
status: code === 0 ? "success" : "failure",
|
|
310
|
+
task,
|
|
311
|
+
terminalOutput
|
|
312
|
+
};
|
|
313
|
+
this.#results.set(task.id, result);
|
|
314
|
+
if (code === 0 && task.cache !== false && fingerprint) {
|
|
315
|
+
const hash = hashFingerprint(fingerprint);
|
|
316
|
+
Object.assign(task, { hash });
|
|
317
|
+
await this.#cache.put(hash, terminalOutput, task.outputs, code, fingerprint);
|
|
318
|
+
await this.#cache.setTaskIndex(task.id, hash);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const result = createFailureResult(task, error, startTime);
|
|
323
|
+
this.#results.set(task.id, result);
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
#dryRunResult(task, startTime) {
|
|
328
|
+
const cacheStatus = task.hash ? `[hash: ${task.hash.slice(0, 12)}...]` : "[no hash]";
|
|
329
|
+
const result = {
|
|
330
|
+
code: 0,
|
|
331
|
+
endTime: Date.now(),
|
|
332
|
+
startTime,
|
|
333
|
+
status: "skipped",
|
|
334
|
+
task,
|
|
335
|
+
terminalOutput: `DRY RUN ${cacheStatus}`
|
|
336
|
+
};
|
|
337
|
+
this.#results.set(task.id, result);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export { TaskOrchestrator };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const calculateProjectDepths = (projectGraph) => {
|
|
2
|
+
const depths = /* @__PURE__ */ new Map();
|
|
3
|
+
const visited = /* @__PURE__ */ new Set();
|
|
4
|
+
const calculateDepth = (projectName) => {
|
|
5
|
+
if (depths.has(projectName)) {
|
|
6
|
+
return depths.get(projectName);
|
|
7
|
+
}
|
|
8
|
+
if (visited.has(projectName)) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
visited.add(projectName);
|
|
12
|
+
const deps = projectGraph.dependencies[projectName] ?? [];
|
|
13
|
+
let maxDepth = 0;
|
|
14
|
+
for (const dep of deps) {
|
|
15
|
+
maxDepth = Math.max(maxDepth, calculateDepth(dep.target) + 1);
|
|
16
|
+
}
|
|
17
|
+
depths.set(projectName, maxDepth);
|
|
18
|
+
visited.delete(projectName);
|
|
19
|
+
return maxDepth;
|
|
20
|
+
};
|
|
21
|
+
for (const projectName of Object.keys(projectGraph.nodes)) {
|
|
22
|
+
calculateDepth(projectName);
|
|
23
|
+
}
|
|
24
|
+
return depths;
|
|
25
|
+
};
|
|
26
|
+
class TaskScheduler {
|
|
27
|
+
#taskGraph;
|
|
28
|
+
#maxParallel;
|
|
29
|
+
#completedTasks = /* @__PURE__ */ new Set();
|
|
30
|
+
#runningTasks = /* @__PURE__ */ new Set();
|
|
31
|
+
#totalTasks;
|
|
32
|
+
#dependentCounts;
|
|
33
|
+
#projectDepths;
|
|
34
|
+
constructor(taskGraph, projectGraph, maxParallel = 3) {
|
|
35
|
+
this.#taskGraph = taskGraph;
|
|
36
|
+
this.#maxParallel = maxParallel;
|
|
37
|
+
this.#totalTasks = Object.keys(taskGraph.tasks).length;
|
|
38
|
+
this.#dependentCounts = this.#calculateDependentCounts();
|
|
39
|
+
this.#projectDepths = calculateProjectDepths(projectGraph);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the next batch of tasks that are ready to execute.
|
|
43
|
+
*/
|
|
44
|
+
getNextBatch() {
|
|
45
|
+
const availableSlots = this.#maxParallel - this.#runningTasks.size;
|
|
46
|
+
if (availableSlots <= 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const readyTasks = this.#getReadyTasks();
|
|
50
|
+
const sortedTasks = this.#sortByPriority(readyTasks);
|
|
51
|
+
return sortedTasks.slice(0, availableSlots);
|
|
52
|
+
}
|
|
53
|
+
startTask(taskId) {
|
|
54
|
+
this.#runningTasks.add(taskId);
|
|
55
|
+
}
|
|
56
|
+
completeTask(taskId) {
|
|
57
|
+
this.#runningTasks.delete(taskId);
|
|
58
|
+
this.#completedTasks.add(taskId);
|
|
59
|
+
}
|
|
60
|
+
isComplete() {
|
|
61
|
+
return this.#completedTasks.size === this.#totalTasks;
|
|
62
|
+
}
|
|
63
|
+
get remainingCount() {
|
|
64
|
+
return this.#totalTasks - this.#completedTasks.size;
|
|
65
|
+
}
|
|
66
|
+
get runningCount() {
|
|
67
|
+
return this.#runningTasks.size;
|
|
68
|
+
}
|
|
69
|
+
#getReadyTasks() {
|
|
70
|
+
const ready = [];
|
|
71
|
+
for (const [taskId, task] of Object.entries(this.#taskGraph.tasks)) {
|
|
72
|
+
if (this.#completedTasks.has(taskId) || this.#runningTasks.has(taskId)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const deps = this.#taskGraph.dependencies[taskId] ?? [];
|
|
76
|
+
if (deps.every((dep) => this.#completedTasks.has(dep))) {
|
|
77
|
+
ready.push(task);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return ready;
|
|
81
|
+
}
|
|
82
|
+
#sortByPriority(tasks) {
|
|
83
|
+
return [...tasks].toSorted((a, b) => {
|
|
84
|
+
const aDeps = this.#dependentCounts.get(a.id) ?? 0;
|
|
85
|
+
const bDeps = this.#dependentCounts.get(b.id) ?? 0;
|
|
86
|
+
if (aDeps !== bDeps) {
|
|
87
|
+
return bDeps - aDeps;
|
|
88
|
+
}
|
|
89
|
+
const aDepth = this.#projectDepths.get(a.target.project) ?? 0;
|
|
90
|
+
const bDepth = this.#projectDepths.get(b.target.project) ?? 0;
|
|
91
|
+
if (aDepth !== bDepth) {
|
|
92
|
+
return bDepth - aDepth;
|
|
93
|
+
}
|
|
94
|
+
return a.id.localeCompare(b.id);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
#calculateDependentCounts() {
|
|
98
|
+
const counts = /* @__PURE__ */ new Map();
|
|
99
|
+
for (const taskId of Object.keys(this.#taskGraph.tasks)) {
|
|
100
|
+
counts.set(taskId, 0);
|
|
101
|
+
}
|
|
102
|
+
for (const deps of Object.values(this.#taskGraph.dependencies)) {
|
|
103
|
+
for (const dep of deps) {
|
|
104
|
+
counts.set(dep, (counts.get(dep) ?? 0) + 1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return counts;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { TaskScheduler };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
exec
|
|
22
|
+
} = __cjs_getBuiltinModule("node:child_process");
|
|
23
|
+
const {
|
|
24
|
+
mkdir,
|
|
25
|
+
writeFile,
|
|
26
|
+
readFile,
|
|
27
|
+
rm
|
|
28
|
+
} = __cjs_getBuiltinModule("node:fs/promises");
|
|
29
|
+
import { join } from '@visulima/path';
|
|
30
|
+
import { FileAccessTracker, generatePreloadScript } from './FileAccessTracker-CrtBAt5D.js';
|
|
31
|
+
import { u as uniqueId } from './utils-zO0ZRgtf.js';
|
|
32
|
+
|
|
33
|
+
class TrackedTaskExecutor {
|
|
34
|
+
#tracker;
|
|
35
|
+
#workspaceRoot;
|
|
36
|
+
/** Tracks active child processes for cleanup on abort */
|
|
37
|
+
#activeProcesses = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(workspaceRoot) {
|
|
39
|
+
this.#workspaceRoot = workspaceRoot;
|
|
40
|
+
this.#tracker = new FileAccessTracker(workspaceRoot);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if file access tracking is supported on the current platform.
|
|
44
|
+
* strace tracking (Linux) or preload script (any Node.js process).
|
|
45
|
+
*/
|
|
46
|
+
// eslint-disable-next-line class-methods-use-this
|
|
47
|
+
get isTrackingSupported() {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if the platform supports full syscall-level tracking (strace).
|
|
52
|
+
*/
|
|
53
|
+
get isStraceSupported() {
|
|
54
|
+
return this.#tracker.isSupported();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Executes a task command and tracks all file system accesses.
|
|
58
|
+
*
|
|
59
|
+
* On Linux, uses strace for comprehensive tracking.
|
|
60
|
+
* On other platforms, uses a Node.js preload script (for Node processes).
|
|
61
|
+
*/
|
|
62
|
+
async execute(task, options, command) {
|
|
63
|
+
const cwd = options.cwd ?? (task.projectRoot ? join(this.#workspaceRoot, task.projectRoot) : this.#workspaceRoot);
|
|
64
|
+
if (this.#tracker.isSupported()) {
|
|
65
|
+
const trackingResult = await this.#tracker.track(command, {
|
|
66
|
+
cwd,
|
|
67
|
+
env: options.env
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
accesses: trackingResult.accesses,
|
|
71
|
+
code: trackingResult.code,
|
|
72
|
+
terminalOutput: trackingResult.output
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return this.#executeWithPreload(command, cwd, options.env);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Executes a command with a Node.js preload script to track fs accesses.
|
|
79
|
+
* Works on macOS, Windows, and Linux for Node.js-based commands.
|
|
80
|
+
*/
|
|
81
|
+
async #executeWithPreload(command, cwd, env) {
|
|
82
|
+
const cacheDirectory = join(this.#workspaceRoot, "node_modules", ".cache", "task-runner");
|
|
83
|
+
await mkdir(cacheDirectory, { recursive: true });
|
|
84
|
+
const id = uniqueId();
|
|
85
|
+
const logFile = join(cacheDirectory, `preload-${id}.log`);
|
|
86
|
+
const preloadFile = join(cacheDirectory, `preload-${id}.mjs`);
|
|
87
|
+
const scriptContent = generatePreloadScript(logFile);
|
|
88
|
+
await writeFile(preloadFile, scriptContent);
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const child = exec(
|
|
91
|
+
command,
|
|
92
|
+
{
|
|
93
|
+
cwd,
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
...env,
|
|
97
|
+
NODE_OPTIONS: `${process.env["NODE_OPTIONS"] ?? ""} --import ${preloadFile}`.trim()
|
|
98
|
+
},
|
|
99
|
+
maxBuffer: 50 * 1024 * 1024
|
|
100
|
+
},
|
|
101
|
+
async (_error, stdout, stderr) => {
|
|
102
|
+
this.#activeProcesses.delete(child);
|
|
103
|
+
let accesses = [];
|
|
104
|
+
try {
|
|
105
|
+
const logContent = await readFile(logFile, "utf8");
|
|
106
|
+
accesses = this.#parsePreloadLog(logContent);
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
await rm(logFile, { force: true }).catch(() => {
|
|
110
|
+
});
|
|
111
|
+
await rm(preloadFile, { force: true }).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
resolve({
|
|
114
|
+
accesses,
|
|
115
|
+
code: child.exitCode ?? 1,
|
|
116
|
+
terminalOutput: stdout + stderr
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
this.#activeProcesses.add(child);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Kills all active child processes. Called on abort/signal to prevent orphans.
|
|
125
|
+
*/
|
|
126
|
+
killAll() {
|
|
127
|
+
this.#tracker.killAll();
|
|
128
|
+
for (const child of this.#activeProcesses) {
|
|
129
|
+
try {
|
|
130
|
+
child.kill("SIGTERM");
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.#activeProcesses.clear();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Parses the JSON-lines log produced by the preload script.
|
|
138
|
+
*/
|
|
139
|
+
#parsePreloadLog(content) {
|
|
140
|
+
const accesses = [];
|
|
141
|
+
const seen = /* @__PURE__ */ new Set();
|
|
142
|
+
for (const line of content.split("\n")) {
|
|
143
|
+
if (!line.trim()) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const entry = JSON.parse(line);
|
|
148
|
+
if (entry.path && !seen.has(entry.path)) {
|
|
149
|
+
seen.add(entry.path);
|
|
150
|
+
if (entry.path.startsWith(this.#workspaceRoot)) {
|
|
151
|
+
accesses.push({
|
|
152
|
+
path: entry.path,
|
|
153
|
+
type: entry.type
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return accesses;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { TrackedTaskExecutor };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRequire as __cjs_createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const __cjs_require = __cjs_createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
const __cjs_getProcess = typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" ? globalThis.process : process;
|
|
6
|
+
|
|
7
|
+
const __cjs_getBuiltinModule = (module) => {
|
|
8
|
+
// Check if we're in Node.js and version supports getBuiltinModule
|
|
9
|
+
if (typeof __cjs_getProcess !== "undefined" && __cjs_getProcess.versions && __cjs_getProcess.versions.node) {
|
|
10
|
+
const [major, minor] = __cjs_getProcess.versions.node.split(".").map(Number);
|
|
11
|
+
// Node.js 20.16.0+ and 22.3.0+
|
|
12
|
+
if (major > 22 || (major === 22 && minor >= 3) || (major === 20 && minor >= 16)) {
|
|
13
|
+
return __cjs_getProcess.getBuiltinModule(module);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// Fallback to createRequire
|
|
17
|
+
return __cjs_require(module);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export { c as collectFiles, a as createFailureResult, h as hashFile, b as hashStrings, r as readPackageDeps, d as resolveTaskCwd, s as sortObjectKeys, u as uniqueId } from './utils-zO0ZRgtf.js';
|
|
22
|
+
import '@visulima/path';
|